Don't defer in tests
Deferring work is a useful mechanism in Go. The defer
keyword lets you register code that you want to run at the end of a function, regardless of how you got there. It’s often used for cleaning up things like closing open I/O streams, finalizing or rolling back database transactions, or decorating errors in a uniform way.
When writing tests, you’ll likely encounter things you want to do and then undo at the end of the test. For example, if you set an environment variable, you’ll likely want to unset it or restore its value back to how it was at the conclusion of the test. And importantly, you don’t care whether the test was successful or not: you want the original state restored regardless.
At first, defer
might seem like the right fit for the job. After all, it usually performs cleanup functions in program code: closing streams, tidying up files, and the like.
The problem is, while it’s often possible to structure your program in such a way that deferred code is easily matched with their respective function’s scope, tests can be messy. It’s not uncommon to have helper functions in test code, and those helper functions may themselves do some setup that you want to tear down. But it doesn’t make sense to defer
the teardown code inside a helper function - that would cause it to tear down whatever it had just set up!
Fortunately, Go provides a way of interacting with the scope of an entire test rather than just one individual function. testing.T
, the same mechanism you already use to report on test errors, has lots of useful helper functions to do exactly this sort of work.
One of those functions is t.Cleanup
, which you can think of as a test-scoped defer.
Use it in the same way you would a normal defer
, but pass the function you want to defer to the end of the test as an argument to t.Cleanup
.
1
2
3
4
5
6
7
8
9
10
11
var sleep = time.Sleep
func TestSomethingSleepy(t *testing.T) {
origSleep := sleep
var timeSlept time.Duration
sleep = func(d time.Duration) { timeSlept.Add(d) }
// Don't do this:
// defer func() { sleep = origSleep }()
// Do this instead:
t.Cleanup(func() { sleep = origSleep })
}
You may have noticed that many test libraries take a testing.T
themselves. Part of the reason for this is to interact with the test-wide scope, sometimes by registering their own t.Cleanup()
steps, or by calling t.Fatal()
as a way to avoid panicking when hitting a condition that should stop the test.
You can make use of this mechanism yourself, too, by having your own helpers register t.Cleanup()
steps.
By the way, lots of common “do and undo” test helpers are already covered by testing.T
without having to write your own t.Cleanup()
logic! t.Setenv
, for example, will set an environment value for the duration of the test, then undo it at the end. t.Chdir
will change the working directory, but again, only for the duration of the current test. And t.TempDir
manages the lifecycle of a temporary directory for you, returning a path that’s ready for use in your test, then deletes it once the test is over.
One final tidbit: when writing helper functions, it’s best practice to mark them with a call to t.Helper()
. This gives the test reporting a hint to skip printing line and file information.