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 one function body is 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.
See more:
- https://dave.cheney.net/2019/05/07/prefer-table-driven-tests
- https://dev.to/boncheff/table-driven-unit-tests-in-go-407b
- https://yourbasic.org/golang/table-driven-unit-test/
- https://www.google.co.uk/search?q=golang+table+tests
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 byreflect.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
- Add
//go:build unit
at the top of the unit test files. - Add
//go:build integration
at the top of integration test files. - For helpers used in both, you can define
//go:build unit | integration
to ensure the file is compiled for both - To run these tests, you must include the
-tags
option 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.
For example:
|
|
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.
The 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
init
if 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