Golabs-III, Mocking collaborators while TDDing in Go
Last updated: Apr 21, 2017
Introduction
In the third edition of Golabs, we will be exploring the program design and plumbing of a package while doing TDD in Go.The design of any package
directly affects ease to test it and hence has a direct impact on the quality.
How often do we hear “The code is difficult to test!” as excuse for not kickstarting TDD or often the reason for abandoning it midway?I am guessing it is more than once for the most of you. Lets do something about it.
Ability to ease collaborators’ mocking during testing is often a (good) symptom of properly designed package.There is a high probability that the package will exhibit high cohesion and low coupling with other packages, dependency graphs will be flat and wide and all the other hallmarks of clean code.
In this Golabs, we will work on designing and writing testable code and map it back to how this discipline facilitates clean code.
A Recap
In the previous edition, we created a real world signup API by writing tests up front.You can check the code out here.
It gave us a starting point on how to write tests for a real world API.
However it had the following limitations:
-SignUpUser() is tightly coupled to the instance of InMemoryUserDao:
Even though we have created an interface, we create a concrete instance of the InMemoryUserDao
Since we have not decoupled or haven’t leveraged the interface driven design properly, any change in the implementation of the InMemoryUserDao will cause SignUpUser to change.This is a clear cut violation of the Single Responsibility Principle.
We need to decouple the SignUpUser from the concrete implementation of InMemoryUserDao and facilitate dependency inversion.
-The entire test execution depends on the particular sequence of the tests being ran. (For Eg: The test for checking whether the code will signal a CONFLICT error code when an user tries to sign up with an already registered email will fail unless there is an actual registration done with the aforementioned email in a previous test)This is called non-determinism of tests and is something that is not desirable at all.
We want our tests to be reproducible and dependable all of the time.
-While testing we are actually modifying the state of the userDataStore which is a package level var. Aside from it not being thread safe, this design will cause us a lot of pain when there is a resource more critical than the in memory array. (What if this API was to provision EC2 instances?We can’t dream of actually provisioning instances during each test run,can we?).
We need the code to be designed in such a way that during tests, it should not have any undesired side effects.
-We also discovered our routes were not covered by tests. It is always a good practice to cover our bases wrt to the contract that we say we adhere to.In other words,
we need to write the tests for our routes as well so that we don’t wreak havoc amongst our consumers due to an unintended regression in API routes mapping.
Some Concepts and Definitions
Lets familiarise ourselves with some terminology before diving into the code.
Collaborator
A collaborator can be defined as a dependency of the SUT that provides an abstracted behavior (often in form of a public API) that the SUT requires to fulfill its job.
Mock
As per Martin Fowler’s excellent article, Mocks are objects pre-programmed with expectations which form a specification of the calls they are expected to receive.
Mocking a collaborator simply means, making assumptions and modeling the behavior of dependencies in our test setup as required by the SUT so that the it can be tested in isolation.
For eg:
InMemoryUserDao is the collaborator/dependency for the signup API.In order to safely and deterministically test out our Signup API, we need to be able to model/mock out the behavior of the InMemoryUserDao in each run as per the condition that we are testing and eliminate the need to actually interact with an instance of the same during testing.
Rule of Thumb
It has been highlighted since the beginning of time that the program design should be interface driven as a good software practice. The reason being it is very easy then to mock out interfaces than concrete implementations. It cant get any truer in Go. If there is one adage that facilitates your code to be testable it is this:
Always Code to Interfaces
Lets Decouple!
We will decouple the concrete implementation of the InMemoryUserDao as follows:
Listing 1
(File: api.go)
var dao UserDao
//SetUserDao sets the userDao.probably not the most thread safe
func SetUserDao(passedDao UserDao) {
dao = passedDao
}
//SignUpUser exposes the signup API
func SignUpUser(w http.ResponseWriter, r *http.Request) {
reqBody := &UserSignUpRequest{}
parseRequestBody(r.Body, reqBody)
//check for valid email address
if !strings.Contains(reqBody.Email, "@") {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("Please enter a valid email id"))
return
}
//check for the length of the password
if len(reqBody.Password) < 8 {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("Password must be at least 8 characters long"))
return
}
//notice how we removed the coupling of the concrete implementation of the InMemoryUserDao?
//var dao UserDao = &InMemoryUserDao{}
_, uniqueViolationErr := dao.isEmailIDUnique(reqBody.Email)
if uniqueViolationErr != nil {
w.WriteHeader(http.StatusConflict)
w.Write([]byte("This email has already been taken"))
return
}
dao.saveUser(reqBody)
w.WriteHeader(http.StatusOK)
w.Write([]byte("You have successfully signed up"))
}
Highlights
- We removed the instantiation of a concrete type ofInMemoryUserDao in our code.- Instead, we expose a setter function to set a private variable
dao which of the UserDao (remember the interface) type.- We then use this
dao variable in the SignUpUser().- This will allow us freedom from creating a instance of
InMemoryUserDao in our method and allow us to use the implementation that is set by the caller by calling SetUserDao(passedDao UserDao)- This can be either called by passing a instance of
InMemoryUserDao during actual executiong or a instance of MockedUserDao during testing as we shall soon see.(Please note that both these conform to the UserDao interface).
Now we have our collaborators designed and consumed via interfaces.
Lets Mock!
In the JVM world, it is easy to have your collaborators mocked by using Spock or Mockito
Go also has good packages to mock collaborators like Moq, but to understand whats happening it is always good to peak under the hood.
As mentioned before, dependencies coded to interfaces make testing easy.In our previous exercise, we begun well by creating a interface
UserDao that lays down the contract for all user related operations.Lets see how we can mock out the interface for use in testing.
This is a very elegant trick that is used in Go extensively.
-We create a struct MockedUserDao with 2 functions as first class members(remember functions are first class members in Golang) having the same signatures as the ones laid out in the UserDao and make the MockedUserDao implement UserDao by providing implementations having a receiver of the MockedUserDao type.
-In the respective implementations we call the member methods of instances of MockedUserDao that have the same signature as themselves.
Now we are home free to mock out the behavior of the isEmailIDUniqueFunc() and saveUserFunc() by supplying the desired mocked behaviour by instantiating the MockedUserDao struct in each test run.
-The respective functions when invoked delegate the call to the mocked behavior. This seems tricky and confusing at first but it very elegant.
This is a trick that I learnt from Mat Ryer’s blog post about Moq. H/T to him!
Listing 2
(File: api_test.go)
type MockedUserDao struct {
// isEmailIDUniqueFuncFunc mocks the isEmailIDUniqueFunc function.
isEmailIDUniqueFunc func(email string) (bool, error)
// saveUserFuncFunc mocks the saveUserFunc function.
saveUserFunc func(u *UserSignUpRequest)
}
// make MockedUserDao implement UserDao and encapsulate the mocked implementations at the same time
func (mock MockedUserDao) isEmailIDUnique(email string) (bool, error) {
if mock.isEmailIDUniqueFunc == nil {
panic("moq: UserDaoMock.isEmailIDUniqueFunc is nil but was just called")
}
//deletage the call to the actual mock implementation
return mock.isEmailIDUniqueFunc(email)
}
func (mock *MockedUserDao) saveUser(u *UserSignUpRequest) {
if mock.saveUserFunc == nil {
panic("moq: UserDaoMock.saveUserFunc is nil but was just called")
}
//deletage the call to the actual mock implementation
mock.saveUserFunc(u)
}
With this done, we are almost done with the plumbing of the mock objects.All that is left to do is to supply the respective implementations in each test run.We can do so by introducing a new field on mockedUserDao which is an instance of MockedUserDao in our test table signUpTests
Listing 3
(File: api_test.go )
var signUpTests = []struct {
Title string
Request UserSignUpRequest
Response UserSignUpResponse
StatusCode int
Message string
URLPath string
mockedUserDao MockedUserDao
}{{
Title: "System should sign up a user and return a OK status when a unique email_id,proper password is supplied",
Request: UserSignUpRequest{UserName: "user1", Email: "user1@gmail.com", Password: "test1234"},
Response: UserSignUpResponse{UserName: "user1", UserID: 1},
StatusCode: 200,
Message: "You have successfully signed up",
URLPath: "/users/new",
mockedUserDao: MockedUserDao{
isEmailIDUniqueFunc: func(email string) (bool, error) {
return false, nil
},
saveUserFunc: func(u *UserSignUpRequest) {
},
},
},
{
Title: "System should throw a BAD_REQUEST when an invalid email_id is supplied",
Request: UserSignUpRequest{UserName: "user1", Email: "user1", Password: "test1234"},
Response: UserSignUpResponse{},
StatusCode: 400,
Message: "Please enter a valid email id",
URLPath: "/users/new",
mockedUserDao: MockedUserDao{
isEmailIDUniqueFunc: func(email string) (bool, error) {
return false, nil
},
saveUserFunc: func(u *UserSignUpRequest) {
},
},
},
{
Title: "System should throw a CONFLICT error when an existing email is supplied",
Request: UserSignUpRequest{UserName: "user1", Email: "user1@gmail.com", Password: "test1234"},
Response: UserSignUpResponse{},
StatusCode: 409,
Message: "This email has already been taken",
URLPath: "/users/new",
mockedUserDao: MockedUserDao{
isEmailIDUniqueFunc: func(email string) (bool, error) {
return false, errors.New("This email has already been taken")
},
saveUserFunc: func(u *UserSignUpRequest) {
},
},
},
{
Title: "System should throw a BAD_REQUEST when a password with less than 8 characters in supplied",
Request: UserSignUpRequest{UserName: "user1", Email: "user3@gmail.com", Password: "test12"},
Response: UserSignUpResponse{},
StatusCode: 400,
Message: "Password must be at least 8 characters long",
URLPath: "/users/new",
mockedUserDao: MockedUserDao{
isEmailIDUniqueFunc: func(email string) (bool, error) {
return false, nil
},
saveUserFunc: func(u *UserSignUpRequest) {
},
},
},
}
The actual mocked implementation can be sourced in each test run as :
Listing 4
for _, fixture := range signUpTests {
//set up http test servers
SetUserDao(&fixture.mockedUserDao)
//other stuff
}
Highlights
- Notice how we make
isEmailIDUniqueFunc() return a error while executing “System should throw a CONFLICT error when an existing email is supplied” .- This removes the need to actually seed a user for this test to pass. (Note that, previously its successful execution depended on the successful execution of “System should sign up a user and return a OK status when a unique email_id, proper password is supplied”)
- We have thus made the tests independent and deterministic.
- You can go through each instance of the
mockedUserDao and map the mocked behavior to each test specification.
This is a handy trick to mock out the Collaborators while testing and will almost always fulfill all your needs.
URL and Routes tests
Remember that we discovered a bug in the system where we tested our API assuming the URL to /users/new but actually the path that was registered was /user/new?
Our Tests passed because we really did not check the sanity of the URLs.It is a good practice to ensure that the our API endpoints are actually available at the place that we promised they will be available.
To do that, we introduce a new field URLPath in our signUpTests table and initialise it to /users/new.(Refer to Listing 2) for details.
And then to verify test it we will do the following
r := InitRouter()
// stuff to setup HTTP
var match mux.RouteMatch
b := r.Match(request, &match)
if !b {
t.Logf("could not find the route %s", fixture.URLPath)
t.Fail()
}
Highlights
- We use theMatch() method of mux.Router.It takes the request object and a reference of the mux.RouteMatch.- The checking is pretty straightforward.Please check the code for more details.
At this point of time we can raise a PR for mocking collaborators and making the test execution independent and deterministic
As always, I recommend cross referencing the content against the code.
What have we achieved?
In this exercise we have been able to get started on mocking of collaborators and dependencies of a SUT. Interfaces in Go provide the flexibility and the power to do so. It is always a good idea to design to interfaces and not concrete implementations.
Yet some more scope for improvement
We have improved on a lot of things from the past exercise, but as they say, there is always room for improvement. The biggest kink to iron out is the fact that the implementation of the UserDao has to be explicitly set by the caller .Our code will panic if this guarantee is not fulfilled by the caller. A trick to do this is to ensure that the caller sets the dependency while calling this method.
As Peter Bourgon says in his observation for best practices in Go
Always Make the Dependencies Explicit
We will see in the upcoming Golabs,how exactly to achieve this and have a dip in the deeper end of the dependency injection pool.
As always,drop in your feedback in the comments section below!
Cheers!