Writing testable code in Golang

Lets try to solve three simple problems to understand how to mock different parts of the code to write good unit tests. These examples try to explain the importance of interfaces and function values in Golang.

NOTE: All the code snippets used here has been tested with Go 1.6.

Three Problems

  1. Send Email via Amazon SES
  2. Get environment variable. Fatal if variable is not set
  3. Simple command to reset User's password stored in database

1. Send Email via Amazon SES

We use gomail package here. The solution is straight forward. We store the SES config in SESMailSender struct, populate the config in init() function from system environment variables and trigger the mail.

2. Get environment variable. Fatal if variable is not set

This problem is simplest of all the three. Sometimes, in your system you don't want your server even to start if some environment variables are not set. This little problem focus on that particular use-case.

3. Simple Command to reset User's password stored in database

If you had used Django web framework, then you would have known how easy it is to change anyone's password via Django shell during the development. This problem tries to do the same kind of operation via simple command. It takes the user's email and new-password then just resets the users' old password with the new one. It does nothing if user with email address doesn't exist.

Though these solutions seems to solve all the problems but how do you make sure the code works in all the edge cases. The answer is via tests particularly unit tests.

But, we cannot write unit tests for any of the above code (at least in a right way). For example
1. How do you test SendInvitationEmail() function without actually triggering actual email.
2. How do you test MustEnv() if it really stops the server in case a particular environment variable is not set? (even test case stops if you tried to test the MustEnv() without setting environment variable because of log.Fatal).
3. How do you write unit test for reset-password without accessing the database?.

Answer is simple. You can't.

The Real Problem

  1. SendEmail() function should be mocked to test the emails functions without triggering email.
  2. MustEnv() should be executed as a different process in tests to check whether it's exiting as fatal if environment variable is not set.
  3. Need to mock the part of the code that access actual database in case of password reset.

In other words, we need to structure our code (both tests and actual code) in a proper way to write proper unit tests.

Wait.. Why does it matter?

why I have to change my code just to write tests?

Tests are important (particularly unit tests). It makes the code base evolve. Tests makes code base maintainable.

Why can't I use some mocking library like we usually do in Python or any other dynamically typed languages?

Yes. You can do it to a certain extent. But, not everything can be mocked at run-time.

Still not convinced?

  1. How do you make sure SendInvitationEmail() not rendering wrong template?.
  2. Is SendInvitationEmail() sending email to all the recipients?
  3. What if recipient email address is empty? will it crash?
  4. How do you make sure the new-password is hashed and updates the user's password?.

So to write proper unit tests you should start writing "testable code" in the first place. But, How do we do that? Here comes the Interfaces and function values of Golang

Let's Solve it

1. Send Email via Amazon SES

The idea is to separate the logic of sending email to interface. We create a Sender interface with Single method SendEmail(). Any type that implements SendEmail() method with exact signature can be used as Sender. So we mock Sender via FakeSender to avoid triggering actual mail and just store it in sender fields so that we can assert in the test cases. Even one can create ConsoleSender with SendEmail() method that just prints the mail template in console rather than triggering email (useful for development purpose).

Here is the actual code that tests SendInvitationEmail() function.

2. Get environment variable. Fatal if variable is not set

Here the scenario is different. We are not going the change any of the code. But, we use exec package to test MustEnv() as a separate process and check whether it's exiting abnormally if particular environment variable is not set. This technique has been inspired from great talk by Andrew Gerrand[1] in Testing Techniques.

Here is the way to test the failure case of MustEnv() using exec package.

3. Simple Command to reset User's password stored in the database

Writing tests for this problem is a bit more tricky.
First we need to move the password reset logic to separate function from main() function as below.

Now whole idea is to unit test ResetPassword() function without accessing the database.

In order to do that, we need to understand what are the places where database is being accessed. There are 2 places.
1. Getting user from database with a particular email address.
2. Storing the new hashed password back into the database.

If we are able to mock this behavior in tests, we are done. Then we can unit test the ResetPassword function without accessing the database.

Here we introduce
1. One anonymous function. func (email string) *models.User.
2. One interface called Saver which contains single method Save()
3. One more anonymous function func (s Saver) that takes Saver interface.

Now ResetPassword() instead of using models.GetUserByEmail() to get actual user, it uses any function that takes email and return *models.User as a user getter. Which is useful because in test we can create fake userGetter to mock this behavior.

Also now ResetPassword() instead of using user.Save() to save user into database, it uses any function that takes Saver interface and call its Save() method to store it. which is again useful because in test we can mock this behaviour by passing fake Saver interface

The resulting code would look like this.

It seems like too much work just to test this password-reset. But, its worth. Now we are sure that password reset works exactly the way it is supposed to. We successfully unit tested Password Reset.

In fact, we successfully unit tested all the three solutions.

Let us know your feedback in the comments.

References

  1. Testing Techniques by Andrew Gerrand