Testing in Go — Some tools you can use

Andrew Davis Escalona
12 min readNov 9, 2020

--

I’ve been working with the Go programming language for almost a year now. As someone who comes from the Javascript world, there have been things I’ve found easier, others that I’ve found harder, and some others have just been interesting.

Unit testing in Go is one of those things I’ve found interesting (and sometimes quite hard too). With Javascript, I could just mock almost everything using Jest. But because Go is a strictly typed language, that was no longer an option. It took me a while to figure some things out.

Today I want to write about some tools I’ve found helpful to use when testing in Go. Some of them are very popular with the Golang community, but some others not so much. Bear in mind that I’m still not an expert with this language. So take everything that I write here with a grain of salt. Let’s begin!

A basic knowledge of the Go language is required for understanding this article.

The Go Standard Library

The first tool I want to talk about is the one you get out of the box. The Testing package comes with all the core capabilities required for setting up your testing suite.

In order to use it, first, you have to create a file with a _test suffix. Let’s see this through an example.

Let’s suppose we have the following function in a util.go file:

This function is quite easy to understand. It will return true or false whether the input value is even or not. This function is also within a package called utilso that it can be exported easily to any part of our project.

The next step is to create a util_test.go file, which will contain all of our test functions and cases. A good rule of thumb is trying to keep one test file per Go file (that contains functionality), making it easier to identify where the test cases of any given function are located. Initially, our util_test.go file would have the following content:

As you can see, we’re already importing the testing package. The t argument is a Go struct that we’re going to use to run all of our tests and assertions, as well as reporting any errors found. In addition to that, any test should start with the word “Test,”, otherwise, the test runner will not recognize that function as a Test. As a personal convention, I like to name my test functions in the form of Test_<Function_Name> so that it’s easy for me to identify which test belongs to which function.

So far this test function is doing pretty much nothing. So how do we define our tests? If you have already taken some Go tutorials, you’ve probably stumbled upon the following design pattern:

With this approach, all test cases are being defined using the tests slice. Each test consists of a string name , an input argument (number) and the expected return value ( want ). Then we proceed to iterate over this slice by using the t.Run method, which will run every test case. Finally, we use if-statements to perform our intended assertions and the t.Errorf method to report any error found.

Honestly, I dislike the previous approach. It’s too verbose, difficult to read and it doesn’t work well when you have to include specific test cases that don’t follow a given pattern. The only good thing about this strategy is that you keep your tests in a very DRY form. However, I always prefer to use the following approach:

In my humble opinion, the approach above is way cleaner than the previous one. What we lose by violating the DRY rule, we gain in better readability, which in the end I consider more important to have when working with Unit tests. Nevertheless, this is just me. If you prefer the first approach, it’s OK! Go with it.

Running your tests

There are several ways to run your tests. For example, if you want to run all the tests in all your packages, do this:

$ go test ./...

You must get the following output in your terminal:

If you also want to get a detailed description of each test and not only the final results, then you should do this:

$ go test -v ./...

This will give you the following output:

Sometimes you only want to run tests for a specific package. In order to do this, you need to explicitly define the targeted package as an argument:

$ go test -v github.com/Andrew4d3/go-testing-examples/util

If you are using VS Code, you are able to use these convenient controls in your test files that will help you to easily run and debug any test you have:

For each test function
For all your package or file

Better assertions and mocks with Testify

For many Go developers, the testing package from the standard library comes with all the required features for building your tests. However, I consider this package to be lacking some important features, for example:

  • More declarative assertions — I’m not a big fan of using if-statements to perform assertions.
  • Some way to mock interfaces easily.

Testify is an external library that comes with these features — and more. Let’s see how we can use it. We’re going to use the same targeted function from the previous part.

First of all, we need to install Testify. Since it’s not a standard package we need to enter the following command in the terminal:

$ go get github.com/stretchr/testify

By doing this, you can use all the packages provided by this library.

Assertions with Testify

Asserting with Testify is quite simple:

You can compare the code chunk above with the one we had before. We got rid of those ugly if-statements and replaced them with a more declarative method called Equal . We will also get more descriptive messages if one of our assertions fails. For example, if we change the first assertion to expect false rather than true , we will get the following message in the terminal:

By looking at this output it’s easy for us to determine what went wrong in our tests.

There are several other assertion methods that you can use in Testify. You can check the official docs to learn more about it. But the ones I’ve been using more frequently are these:

  • Equal to assert equality
  • Error to assert the given value is of type error
  • Errorf to assert the given value is an error and also contains the given message
  • Contains to assert the given string contains the provided substring
  • Nil to assert the given value is nil
  • And some other I can’t remember now…

Mocking with Testify

When unit testing our functions, sometimes we come across situations where we want to mock the output of a method and also assert its input. Probably because we want to bypass some side-effect operation that our method is carrying out. Testify comes with a mock package that helps us achieve just that.

Let’s see this through an example. Imagine you are developing an API for a bank, and one of the features you need to implement is getting the account balance from a specific bank account ID. So we come up with the following solution:

Obviously, we’re abstracting some details here, but you get the idea. We have a BankClient interface that defines the method GetBalanceByID . This method is responsible for returning the balance from any given account using its ID. In the last part, we have an implementation of such a method. In the real world it should perform some operations with a database or external API, but for the sake of simplicity, here we’re just returning a random number.

Now, let’s imagine we’re asked to implement a function that adds the balance from two given accounts. Our function should receive both account IDs and return the resulting sum from them. Something like this:

Now the question here is: How do we unit-test this? So far, our major obstacles are those two GetBalanceByID calls. We must find a way to mock those calls so that we can make the SumAccountBalance function behave in a deterministic way. How can we achieve this? By using Testify of course.

Using the mock package, we can set our expectations and define how our mocked bank connection should behave:

First, we need to define a mocked bank connection struct, which is inheriting its attributes from the Testify library ( mock.Mock ). After that, we define a “fake” GetBalanceByID method, which is responsible for wiring the returning values defined by us.

Next, in our first test case, we need to define a (mocked) bank connection object and set its expectations. The first expectation is: “return balance 1000 when you get ID: 1” and the second: “return balance 2000 when you get ID: 2”. By doing this, we stub the actual method call and avoid any side effects that it might have. Last but not least, we assert our returning values. Notice how we use the method AssertExpectations which will verify whether our mocked methods get called or not.

We can address the rest of the test cases in a similar way:

As you might notice, this testing strategy will only work if we use interfaces for our function and/or method parameters. So my advice would be to avoid passing structs as arguments. Otherwise, we won’t be able to use this Testify feature.

Generating Mocks with Mockery

Defining our mocks manually is not a problem when we only have one or two methods to mock. But what would happen when the project starts growing and now we find ourselves needing to mock several methods for several interfaces? Yes … It’s a nightmare!

Fortunately for us, there is a CLI tool that we can use to generate our mocks automatically without too much trouble. Its name is Mockery.

To install this tool, you only need to follow the instructions found in the Github repository, or you can simply download the binaries found here. Whichever option you choose, verify the tool is correctly installed by entering the following command:

$ mockery --version

Let’s prove this tool by generating a mock for the BankConnection interface from the previous part. Let’s enter this command in the terminal:

$ mockery --dir=util name=BankConnection

NOTE: The command above is assuming the util package is located in a util directory in your project’s root. Remove the --dir attribute if the targeted interface is at root level.

After running the command above you will see how a mocks directory is generated. This directory contains a BankConnection.go file. Let’s check out the content of such a file:

The first thing you might have noticed is how this GetBalanceByID version has more logic than the one we wrote just a few moments ago. It’s because this version is carrying out some extra validation steps that we forgot to take into account (Oops!).

Now we can go back to our test file and replace the test object definition to use our newly generated mocked struct. We can also remove the old struct type definition because we don’t longer need it. Additionally, don’t forget to import the mocks package!

If you run the tests again, they should run without problems.

Generating mocks for external interfaces

Mockery can work perfectly for our interfaces, but what about interfaces that don’t belong to our codebase (external interfaces)? The first example that comes to mind is the context.Context interface. Let’s imagine we have a function that extracts the value from the Go context. Something like this:

If we wanted to unit-test the above function, we would need to find a way to generate a mockedctx that implements the Context interface. Now, I know what you’re thinking: “Why don’t we just generate a fake context using the context.Background() method?” Yes… That could work. But there are situations where that’s not even an option, maybe because using this method might trigger some side-effects that our test suite cannot handle. So we need to find a way to mock this interface up. But how? Manually? No way!

Here is when we need to resort to a little trick. Temporarily, we can define an interface that inherits from the targeted one. Something like this:

// This is just temporarly
type MockedContext interface {
context.Context
}

With our fake interface defined, we can just run the mockery command to generate its corresponding mocks:

$ mockery --dir=util --name=MockedContext

We should see a new MockedContext.go file with all the generated code. Let’s write our tests using these mocks:

After verifying that your tests pass, you can go back and delete the MockedContext interface that we used to “fool” the Mockery CLI tool since we no longer need it.

Monkey-patch: The holy grail of stubbing and mocking

So far, so good. We have been able to mock (or stub) interfaces without problems. But what about struct methods? What about simple functions?

With functions, you have the option to “overwrite” your function calls like this:

// ...var jsonMarshalFn = json.Marshal // or any other function// ...func MyFunction() {   
payload := map[string]string{"foo","bar"}
data, err := jsonMarshalFn(payload)
// ...
}
// And then in your tests...func Test_MyFunction() {
jsonMarshalFn = func(v interface {}) ([]byte, err) {
// Whatever you want to mock or stub here
}

MyFunction()
}

That would definitely do the trick. The only problem is that we would need to go around and replace all our function calls with these function vars. That might require quite a long refactoring if you are deep into the development process. Besides, this approach doesn't work with struct or interface methods. So how can we work around this?

Introducing The Go Monkey Library…

This library does some low-level magic so that you can replace any function, interface, or struct method with any desired mocked version. Let’s see how it works.

First, we need to install the package:

$ go get bou.ke/monkey

Now, let’s suppose we have the following function:

The function above will return the current date and time in ISO format. Unfortunately, this function is not going to work in a deterministic way, which means it will return a different result every time you run it. If you are familiar with the functional programming concepts, you can say such a function is not pure. Unfortunately for our tests we need the opposite: we need the function time.Now() to be deterministic, which means that it needs to return the same date and time every single time. We can achieve this by using our new monkey patch library in this way:

As you might notice, in line 18, we’re replacing the original time.Now function with a substitute function which will always return the same Date object with a fixed date and time. It’s important that our substitute function matches the same signature as our targeted function; otherwise, it won’t work. We’re also doing some cleanup at line 15, by using the method monkey.UnpatchAll. This will get the time.Now function back to its original state.

NOTE: If after running the tests, the patching doesn’t work, you’re going to need to include the following flag into your test command: -gcflags=-1

Monkey-patching methods

As I already mentioned, we can also use Monkey-patch to replace methods from structs. Let’s go back to our GetBalanceByID method. In order to make this method deterministic, we need to stub the method Float64 which belongs to the Rand struct. So we need to do something like this:

As you might notice, here we are monkey-patching the Float64 method to return a fixed value (0.5). By doing this, we can now run an assertion against the expected value of 500.

Not everything is perfect

Unfortunately, this library comes with some caveats:

  • The Github project is archived, so it’s no longer under maintenance
  • It only works with Linux and Mac (sorry Windows users)
  • As stated in the library’s README: “Monkey won’t work on some security-oriented operating systems that don’t allow memory pages to be both write and execute at the same time. With the current approach, there’s not really a reliable fix for this”

This is why I will only recommend using this library when you have no other options. Instead, try to work around interfaces that you can easily mock with Testify. You can also wrap external struct types around others in your code and overwrite their behavior if you require it.

Wrapping up!

And that’s it! I hope you have been able to learn one thing or two about testing in Go. Thanks for reading my article.

Happy coding!

PS: All the code examples can be found here:

--

--