Testing in Golang
Table-driven tests format your tests to ensure that you are as DRY as possible while making it easy to add more test cases. There are plenty of resources about this, which I will link below; however, here is my appreciation.
The key here is that there is one function body doing the tests and that it is easy enough to add a new case by adding the correct data.
If you have more complex data, you can easily add more to the test
struct to model this:
It’s a contrived example, but you can see how you can use more complex data in your tests.
- Notice that you can rely on default values for non pointer values, when writing your tests; this will reduce the repetition of tests. also including a value shows intent
- You can also construct default values for the local structs, if required, by defining a struct before your tests and adding merging the values from the test data into it before starting the work and assertions.
Go ships with bare bones testing framework, which is shocking if you have come from more feature-complete testing platforms. Luckily the community provides!
assert.Equal()will do a deep equal powered by
reflect.DeepEqual, if given two structs. Do not roll out your own unless you have to!
Go’s test runner does not have a built-in concept of suites. Separating unit and integration tests can become an issue.
There is a way to do this with creative use of the build tags. This does come with the caveat that you must change your test commands to select the suite you want.
From go 1.17, the tag is now
//go:build. Versions before used
// +build; both syntaxes are currently valid but prefer the newer, former version
//go:build unitat the top of the unit test files.
//go:build integrationat the top of integration test files.
- For helpers used in both, you can define
//go:build unit | integrationto ensure the file is compiled for both
- To run these tests, you must include the
-tagsoption in the command:
go test -tags unit ./...
go test -tags integration ./...
go test -tags unit,integration ./...
Setting environment variables with
os.SetEnv() and retrieving with
os.GetEnv() in testing can lead to race conditions.
I have found this behaviour in a project where a test ended up asserting a value set in the env in the previous test.
A quick Google found this GitHub Issue which states :
>Unfortunately, people often don’t realize that setting environment variables in tests using
os.Setenv will set those variables for the entire lifetime of the process.
Also without test harness support for env variables, it also becomes a problem in parallel tests.
In order to get around this, make sure to write more testable functions that take a collated version of env vars.
A function written like the above, can be tested with ease bypassing a
map[string][string], without having to worry about the race conditions of access to the env.
testing.T does actually contain a function
t.SetEnv which will set an env var and clean it up after the test for you, using
os.Setenv, however, it still cannot be used in parallel tests. I’d still prefer to use a defined map. It has the added bonus of keeping
See using-and-testing-flag. The TL;DR is:
- Don’t set up flags in
initif you want to write tests
- Don’t use the default flag set if you want to write tests
- Do use a custom flag set and ensure you can pass args to the tests