Better Mocks In Go

When I first started mocking in Go my tests were a mess. I would have various different mocks all mocking the same thing but with different behavior. It quickly became unorganized, hard to follow and hard to maintain. Go is supposed to be clean and concise. But my code was quickly becoming otherwise. Let's take a quick look at what I was doing.

Ugh! What was I thinking?

Let's imagine we have a RecipeService with a function called ListRecipes. ListRecipes goes out to the database and fetches a list of recipes. When unit testing we don't actually want to make any calls to the database. It wouldn't be a true unit test. So we need to mock this RecipeService. Here was my first try.

Not a terrible mock. I wanted to test what would happen if my service returned an error. Here's the test.

type BadRecipeService struct {
	*database.DB
}

func (ps *BadRecipeService) ListRecipes(page int) ([]*better.Recipe, error){
	return nil, errors.New("Oppppps")
}
func TestListRecipes(t *testing.T) {
	req, err := http.NewRequest("GET", "/recipes?page=1", nil)
	if err != nil {
		t.Fatal(err)
	}

	var store mock.BadRecipeService
	handler := Handler{RecipeService: &store}

	rr := httptest.NewRecorder()

	handler.ServeHTTP(rr, req)

	if status := rr.Code; status != http.StatusInternalServerError {
		t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusInternalServerError)
	}
}

A quick test to make sure my call returns the correct status code. Done. But now I want to test what happens when my store doesn't return an error. My first thought was to "oh let's create another mock"

type GoodRecipeService struct {
	*database.DB
}

func (ps *GoodRecipeService) ListRecipes(page int) ([]*better.Recipe, error){
	return []*better.Recipe{{ID: 1, Title: "Pasta"}}, nil
}

And then I would create another test that checks to make sure I get a 200 status. I didn't like how this worked. I'm duplicating my mocks. And that didn't sit right.

My test file would look like this.

type GoodRecipeService struct {
	*database.DB
}

func (ps *GoodRecipeService) ListRecipes(page int) ([]*better.Recipe, error){
	return []*better.Recipe{{ID: 1, Title: "Pasta"}}, nil
}

type BadRecipeService struct {
	*database.DB
}

func (ps *BadRecipeService) ListRecipes(page int) ([]*better.Recipe, error){
	return nil, errors.New("Oppppps")
}

func TestListRecipes(t *testing.T) {
	req, err := http.NewRequest("GET", "/recipes?page=1", nil)
	if err != nil {
		t.Fatal(err)
	}

	var store mock.BadRecipeService
	handler := Handler{RecipeService: &store}

	rr := httptest.NewRecorder()

	handler.ServeHTTP(rr, req)

	if status := rr.Code; status != http.StatusInternalServerError {
		t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusInternalServerError)
	}
}

...

Adding more functions and more services my code quickly became ..... bad.

So whats the better way?

First, I decided to move my mocks into a separate mock package. To keep them all organized in one location. And then I created a single MockRecipeService.

type MockRecipeService struct {
	ListRecipesFunc func(page int) ([]*better.Recipe, error)
}

func (s *MockRecipeService) ListRecipes(page int) ([]*better.Recipe, error) {
	return s.ListRecipesFunc(page)
}

I now have one mock that takes a function. That means I can use the same mock but use different behaviors for different tests. So now my test looks like this.

func TestListRecipes(t *testing.T) {
	req, err := http.NewRequest("GET", "/recipes?page=1", nil)
	if err != nil {
		t.Fatal(err)
	}

	var store mock.MockRecipeService
	store.ListRecipesFunc = func(page int) ([]*better.Recipe, error) {
		return []*better.Recipe{{ID: 1, Title: "Pasta"}}, nil
	}
	handler := Handler{RecipeService: &store}

	rr := httptest.NewRecorder()

	handler.ServeHTTP(rr, req)

	if status := rr.Code; status != http.StatusOK {
		t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
	}
}

For each different test I can swap out the function to test different behavior. No more duplicating mocks. There is one mock for each service I need to test. So much cleaner.

A quick thank you to Ben Johnson @benbjohnson for his suggestions on refactoring my tests.

Join the Newsletter

Subscribe to get our latest content by email.
    We won't send you spam. Unsubscribe at any time.