Go Mock Testing

You should always write tests. To fully understand how to write production level applications in any language you should be able to write unit tests. Some people go to the extreme of doing Test Driven Development (TDD). TDD advocates that you should write tests before you write the code. This can be a good approach when trying to write code that can be easily tested. Yet I find that TDD is a lot like agile; a lot of people say they do it but in reality they do some sort of hybrid version. In the end it boils down to eating your veggies before your steak. Some people love veggies, just like people love writing unit tests, but most people find satisfaction in running their code and seeing it work.

My approach to testing is that I don’t feel comfortable without tests, but like my son might say, “they’re not my favorite”. You will find very few people who don’t see the value in unit tests but most people don’t write good tests. Obviously this is an opinion but good tests typically run fast, actually test the code, and should be robust. Taking a gander at “Ye Olde Testing Pyramid” you will see that you should have a lot of unit tests, a smaller amount of integration tests, and a very small amount of end to end tests. As you move up the latter the tests, by their nature, become brittle because of the increased number of dependencies. They also tend to take longer as you move up the chain.

Certainty Chart

That’s why most of your effort should go into unit testing. Preferably you should be spending most of your effort testing components in isolation to easily pinpoint failures. Now in my post about Hexagonal Architecture I mentioned about making “portable” code. Each port used an interface that led to the domain logic which also used interfaces to interact with ports. When assembled we saw that you can swap in various implementations of the interface and not have to rewrite a bunch of code.

Most books on testing you’ll stumble across this phrase:

Write testable code

Various testing authorities

Which seems obvious, but if you don’t focus on testing first (as TDD zealots preach) often times you will find it very difficult to write tests based on how you wrote the code. Hexagonal architecture typically allows you to have testable code because you can inject mocked dependencies that will allow us to test how business logic should work without trying to configure resources or setup test data.

Setting up Mockgen

Go is such a unique language because testing is a first class citizen built into the framework so support for various types of testing are part of the Go project. So there are a couple different mocking frameworks in Go but we are going to use the one that is part of the main project, Mockgen.

To install Mockgen just run the following (make sure your $GOPATH/bin is part of your source path):

go get github.com/golang/mock/gomock
go install github.com/golang/mock/mockgen

Now that we installed Mockgen we will use it to generate mocks.

On Mocking

Mocks are different than stubs; a stub will allow you to generate expected responses when it is called but will not provide an easy mechanism to verify that something has been called. Mocks will allow you to simulate the interactions with a specific service or method and often provide a verify method that allows the tester to verify interactions.

Mockgen will do most of the heavy lifting for us to generate mocks. Mockgen has a lot of features that I won’t go into here but feel free to look at their documentation for more advanced ways of testing. We will be generating mocks for all of the interfaces we made in the last example.

mockgen -package mocks -destination mocks/ticket.go hex-example/ticket TicketRepository,TicketService,TicketHandler

You can see here we want our output to be in a mocks package and directory with the name of the domain we are mocking. This will be helpful if ever you need to migrate some code. We then define the input package and finally the interfaces we want to mock. Voila, we have mocks.

Test suites

Next we will install a suite package which will allow us to cut down on repeating code for setup and keep our tests clean. We will be using a suite packages called testify. Suites allow you to group tests together and can share a common setup. Tests, like all code, should be readable; tests should also be succinct and targeted. Suites allow you to move some logic to Before and After methods that setup and teardown tests. In this example we want to set up our mocks before each test and don’t really want to repeat the same code over and over.

So we’ll create a new test file in the ticket directory called service_test.go. Depending on your IDE you may have the package predefined at the top of the file. Please make sure that you are using a separate _test package, in this case it should be ticket_test. This is important because you want your test code to be outside of the package you are testing. We do this because we want to treat the package we are testing as a “black box” and by using the *_test package we can ensure we can only access exported identifiers.

Next we will create a test suite struct and an initial run script. Our struct will house any supporting services we may need to use and the service we are going to test. The supporting services in this case will be mocked and used to create the service under test. Personally I like to name the service to be tested underTest to clarify what we are doing in our tests. Finally we need a run method that injects the T testing object into the suite. This is to adhere to the Go testing standards.

func TestTicketServiceSuite(t *testing.T) {
	suite.Run(t, new(TicketServiceTestSuite))
}

type TicketServiceTestSuite struct {
	suite.Suite
	ticketRepo *mocks.MockTicketRepository
	underTest  ticket.TicketService
}

Now we will create a Before method that will allow us to setup the mocks. This will instantiate the mocked repository we defined using Mockgen and create a new service object to be tested.

func (suite *TicketServiceTestSuite) SetupTest() {
	mockCtrl := gomock.NewController(suite.T())
	defer mockCtrl.Finish()

	suite.ticketRepo = mocks.NewMockTicketRepository(mockCtrl)
	suite.underTest = ticket.NewTicketService(suite.ticketRepo)
}

Finally we get to write the tests.

Writing the test

There are many approaches to writing tests but as stated earlier you want to make sure they are clear and concise. I find that the Arrange, Act, Assert methodology helps me keep my tests a little more organized. By defining your tests this way you can easily go through the flow of a test and find out how it works. Looking at the Arrange section allows you to see what the state needed for the test to run. Act is normally just executing the service. Assert then validates what the test was supposed to do. Typically if a test fails it is in an unexpected state causing a bad assertion or something changed in the logic to change the result.

Mocked classes as stated before give you an ability to do assertions on the mocked object itself. In this scenario we will want to verify that a repository method was called (or in some negative test cases not called) and that it will return an expected value. When we arrange the test we will setup the mock and set the scenario in which it’s supposed to act.

t := &ticket.Ticket{
  Creator: "Joel",
}
suite.ticketRepo.EXPECT().Create(gomock.AssignableToTypeOf(&ticket.Ticket{})).Return(nil)

At the end of the test they mock will run through the Expect clause and make its assertions. We can do additional assertions as well so we know that specific service method things happened, in the case of create, we want it to assign an id, created, updated, and ticket status and so we should assert those values have been filled from the result.

func (suite *TicketServiceTestSuite) TestCreate() {
	//Arrange
	t := &ticket.Ticket{
		Creator: "Joel",
	}
	suite.ticketRepo.EXPECT().Create(gomock.AssignableToTypeOf(&ticket.Ticket{})).Return(nil)

	//Act
	err := suite.underTest.CreateTicket(t)

	//Assert
	suite.NoError(err, "Shouldn't error")
	suite.NotNil(t.ID, "should not be null")
	suite.NotNil(t.Created, "should not be null")
	suite.NotNil(t.Updated, "should not be null")

}

We can do this for the rest of the methods and even test error handling if we wanted. The code for testing handlers is very similar except we are going to mock the service instead of the repository and we will have a slightly different way to test the handlers.

Go has a test package for recording HTTP requests sent to a handler that helps capture the response. So here we will setup a new request with an id to test the FindById handler. We will mock the service call but make sure it expects the id we set in the path and we have a struct that will be returned.

func (suite *TicketHandlerTestSuite) TestFindTicketById() {
	t := &ticket.Ticket{
		Creator: "Joel",
	}
	suite.ticketService.EXPECT().FindTicketById("test").Return(t, nil)

	vars := map[string]string{
		"id": "test",
	}

	r, _ := http.NewRequest("GET", "/tickets/test", nil)
	r = mux.SetURLVars(r, vars)

	w := httptest.NewRecorder()
	suite.underTest.GetById(w, r)

	response := w.Result()
	suite.Equal("200 OK", response.Status)

	defer response.Body.Close()
	result := new(ticket.Ticket)
	json.NewDecoder(response.Body).Decode(result)

	suite.Equal("Joel", result.Creator)
}

Again, we can run through other test scenarios for complete coverage.

To run your tests simply type:

go test ./...

Conclusion

Mocked testing is just one type of testing you should be doing for your code but it is the most important. It is testing the business logic of your code and how the different elements interact. In the end you will find that it is the most robust test code you have and is changed only if business logic changes or bugs are found.

Source code can be found here

Related