Deferred teardown closure in Go testing
While watching Mitchell Hashimoto's Advanced Testing with Go talk, I came across this neat technique for deferring teardown to the caller. Let's say you have a helper function in a test that needs to perform some cleanup afterward.
You can't run the teardown inside the helper itself because the test still needs the setup. For example, in the following case, the helper runs its teardown immediately:
When helper is called, it defers its teardown - which executes at the end of the helper function, not the test. But the test logic still depends on whatever the helper set up. So this approach doesn't work.
The next working option is to move the teardown logic into the test itself:
This works fine if you have only one helper. But with multiple helpers, it quickly becomes messy - you now have to manage multiple teardown calls manually, like this:
You also need to be careful with the order: defer statements are executed in LIFO (last-in, first-out) order. So if teardown order matters, this can be a problem. Ideally, your tests shouldn't depend on teardown order - but sometimes they do.
So rather than manually handling cleanup inside the test, have helpers return a teardown function that the test can defer itself. Here's how:
Each helper is self-contained: it sets something up and returns a function to clean up whatever resource it has spun up. The test controls when teardown happens by calling the cleanup function at the appropriate time. Another benefit is that the returned teardown closure has access to the local variables of the helper. So func() can access the helper's testing.T without us having to pass it explicitly as a parameter.
Here's how I've been using this pattern.
Creating a temporary file to test file I/O
The setupTempFile helper creates a temporary file, writes some content to it, and returns the file name along with a teardown function that removes the file.
In the main test:
Running the test displays:
Starting and stopping a mock HTTP server
Sometimes you want to test code that makes HTTP calls. Here's a helper that starts an in-memory mock server and returns its URL and a cleanup function that shuts it down:
And in the test:
Running the test prints:
Setting up and tearing down a database table
In tests that hit a real (or test) database, you often need to create and drop tables. Here's a helper that sets up a test table and returns a teardown function to drop it:
And the test:
The t.Cleanup() method
P.S. I learned about this after the blog went live.
Go 1.14 added the t.Cleanup() method, which lets you avoid returning the teardown closures from helper functions altogether. It also runs the cleanup logic in the correct order (LIFO). So, you could rewrite the first example in this post as follows:
Now the testing package will handle calling the cleanup logic in the correct order. You can add multiple teardown functions like this:
The functions will run in LIFO order. Similarly, the database setup example can be rewritten like this:
Then the helper function is used like this:
Fin!
Discussion in the ATmosphere