How to set up and tear down unit tests in Go
Introduction
When writing unit tests in Go, it's sometimes useful to perform common actions that need to be executed before and after a test. These setup and tear-down tasks might be needed in different scopes:
- Before and after the full test suite is executed.
- Before and after each test is executed.
- Or even before and after a group of tests is executed.
Coming from a Java background, this is a common practice. Testing frameworks such as JUnit have built-in annotations that provide this functionality in an aspect-oriented way. However, achieving the same in Go with the standard testing library has been a bit elusive. In this article, I'll show you how to accomplish these tasks in Go.
Running code before and after the full test suite
Starting with Go 1.4, the standard library provides a way to run code before and after the full test suite is executed. If a test file in a package contains a function with the signature:
func TestMain(m *testing.M)
Go will execute that function instead of running the tests directly. You can learn more about this function in the Go standard library documentation.
A typical implementation of this function would be:
func TestMain(m *testing.M) {
// Setup code goes here
code := m.Run()
// Teardown code goes here
os.Exit(code)
}
This function allows us to run code before and after the full test suite is executed.
To execute the tests, we need to call m.Run()
.
Note how we store the result of m.Run()
in a variable and then call os.Exit(code)
to exit the program with the proper exit code.
Things to consider:
- You can define this function in any of your package test files.
- Function names are unique in a given package, you can only define only one
TestMain
function per package. - If your package contains multiple test files, place the
TestMain
function wherever it makes the most sense. - If you don't call
os.Exit
with them.Run()
return code, your test command will always return0
(even if it fails).
Let's now learn how to run code before and after a group of tests.
Running code before and after a group of tests
Another common scenario is to run code or prepare the testing environment before a group of tests is executed. Go subtests are very useful in this case.
Subtests leverage the t.Run(name string, f func(t *T))
function to run the provided f
function as a subtest of t
.
Let's see an example:
func TestSomethingWhenGivenCondition(t *testing.T) {
// Setup code for given condition goes here
t.Run("Something has property one", func(t *testing.T) {
// Subtest 1 code goes here
})
t.Run("Something has property two", func(t *testing.T) {
// Subtest 2 code goes here
})
// Teardown code for given condition goes here
}
Suppose we want to test the behavior of our application given an environment with specific conditions.
Subtests allow us to group these tests under a function, in this case, TestSomethingWhenGivenCondition
.
This function allows us to set up and tear down the environment by running code before and after the subtest invocations.
We can then run a subtest using the t.Run
function for each property or behavior of the application we want to test.
You can find a complete example at this link. Let us now see how to run code before and after each individual test.
Running code before and after each test
The Go standard testing library doesn't provide a built-in way to run code before and after each test. For this purpose, we need to provide a workaround implementation. This is what I usually do.
Create a struct to hold the test context that will be initialized and torn down before and after each test.
type testContext struct {
some string
test *pkg.Var
context int64
}
func (c *testContext) beforeEach() {
// Setup code for context goes here
}
func (c *testContext) afterEach() {
// Teardown code for context goes here
}
Next, define a function that's used to encapsulate each test:
func testCase(test func(t *testing.T, c *testContext)) func(*testing.T) {
return func(t *testing.T) {
context := &testContext{}
context.beforeEach()
defer context.afterEach()
test(t, context)
}
}
Finally, use this function in a subtest:
func TestSomething(t *testing.T) {
t.Run("Some test case", testCase(func(t *testing.T, c *testContext) {
// Test code goes here which can leverage the context
}))
}
For each test invocation, a new testContext
is created and the beforeEach
and afterEach
functions are called before and after the test is executed.
You can find a complete example at this link.
Conclusion
In this article, we've seen how to properly set up and tear down a Go unit test using Go's standard library for unit testing. We've seen how to run code before and after the full test suite, before and after a group of tests, and before and after each test. These techniques will help you write robust and well-organized tests in your Go projects.
Happy testing!