7 Testing Techniques for Your Golang Codebase
A set of tips that helped us test and build our products reliably

- As soon as your project moves ahead of the PoC stage, you might need to start adding tests.
- Every line of code is subject to be changed, either by a refactor or by the introduction of new functionalities.
- You need a safety net that ensures that new functionality works and that it doesn’t break any previous functionality.
These are general ideas that apply to software regardless of what language is used in the code base. However, each language has its own specific character and this is somehow reflected on the way tests are written.
Here, we would like to share some techniques that help us test our Lang.ai products:
1. Choosing where to place the tests
The first thing to decide is whether to place your tests in the same package or in the *_test package. We use the *_test package as this enforces us to test only the package’s public API and prevents us from messing with the internal implementation of the tested package.
If tests are harder to implement without having access to package internals, you might want to rewrite some parts of your package to make it more testable (see Configuration and Golden files sections below)
Extra: By restricting your tests only to the public API, your tests are less coupled to the actual implementation details and more to the expected functionality, which might lead to easier refactors as your tests only care about the input/output relation.
2. Using helper functions that don’t return an error
Tests tend to have calls to multiple helper functions (like setup or teardown) and these functions are usually the same(json.Marshal, ioutil.ReadFile…). Most of these functions return errors that must be checked, but thisif err != nil {...}
pattern can easily clutter the tests and make them harder to read and understand. For this cases, use helper functions that never return an error (more on this in Mitchell Hashimoto’s talk)
3. Taking advantage of Ginkgo
This one is very opinionated, as we have seen multiple resources advocating not to use any testing frameworks given that Go’s testing package is complete enough and adding an extra framework increments code complexity.
We understand this arguments and we think Ginkgo might be an overhead for some use cases, however we feel it has helped us to maintain a clean and extendable test base. Specially, we make use of setUp/tearDown functions, test cases organization with Describe/Context blocks and the gomega matchers.
4. Using configuration functions
To be able to test your code from the *_test package, you might need to change some internals: instead of accessing the variables directly, use configuration functions.
Avoid const
values that define the behaviour. Use them, instead, as default values and provide configuration functions so they can be changed if needed.
Take a look at the gist below:
5. Using golden files
When you have to deal with complex structures, it becomes exhausting to hardcode the expected result. In these situations, golden files turn out to be very helpful as they drastically reduce your test size and maintenance. If you are familiar with Jest, the idea is similar to the snapshots where the result of the function is compared to a previously saved snapshot. These snapshots are manually updated and committed with your code.
You can use some helper functions to easily check golden files too!
When you want to update your golden files, run the tests with the flag -update
and commit the golden files with your code base.
6. Using fixtures
Golden files are useful when you have to deal with complex output values, but for complex input values we use fixture files. Fixture files are an example of fixtures where your data is stored in files (like .json or .yaml) and helper functions are used to load them.
Another type of fixtures is when you have to work with a pre-populated database. In that case, we find go-testfixtures very handy.
In every package we have the folder testdata/fixtures with different YAML files. The YAML file name must match the table name and the contents must be an array with the table records. We could have a books.yaml file like this:
- id: 1
title: "Pedro Páramo"
author: "Juan Rulfo"
- id: 2
title: "Meditations"
author: "Marcus Aurelius"
- id: 3
title: "Walden"
author: "Henry David Thoreau"
Then, in our tests we just need to add the following:
7. Mocking external services (httptest.Server)
If your code relies on external services, you don’t want your unitary tests to make external requests. For these situations, net/http/httptest package exports a type Server
that listens on a system-chosen port on the local loopback interface.
Here it is a simple example where we just mock a server to avoid external traffic:
Here it is a more complex case where we mock an external service and test a full integration so we make sure the right requests are made and the responses are correctly processed.
Further resources
Thank you for reading this article. If you found it interesting, here are some resources that helped us:
Check the other articles in our Building Lang.ai publication. We write about Machine Learning, Software Development, and our Company Culture.