Go v1.14 ships with improvements across different aspects of the language. Two
of them are brand new
b.Cleanup methods, added to the
The introduction of these methods will make it easier for our tests to clean up
after themselves. This was always achievable through the careful composition of
(sub)tests and helper functions, but since Go 1.14, the
testing package will
ship with one right way to do that.
Let’s explore through a scenario, one very common in web development, how these methods work, and how to put them in use.
A web service that we own has an authentication middleware that uses HTTP
Basic Authentication. The middleware takes the
Authorization header values of
an HTTP request using the
method and authenticates
the request. Depending on the authentication result, it will return an error
response or let the request go through.
AuthMiddleware authorization middleware:
AuthMiddleware is a
struct that has a
db attribute of the type
*gorm.DB, which, in fact, is a Gorm database connection
Validate method of the middleware will check the validity of the
authorization credentials sent by the client. (Yes, this is a very dumb and
vulnerable service - stores plain text passwords in the database. No, you
should never keep your customers' passwords in plain text.)
To do that, it uses the
method that returns the
username and password provided in the request’s
Authorization header. Then,
we invoke the
Validate method with
u (the username) and
p (the password)
as arguments, that returns a
When the request successfully authorized, the response of the endpoint will be
returned. If the request does not supply proper HTTP Basic Authentication
credentials, or the supplied credentials are invalid, it will return an
HTTP 403 Forbidden error.
The simplistic server that builds the router and mounts the handler and the middleware:
main function opens a Gorm connection to an
SQLite3 database, builds a
mux router, and mounts a handler at the
/foo URI. Then, it mounts the
router on a server and starts listening on
So, a sensible question would be, how can we test such a middleware? Since
there is a database connection in play here, we have to make sure the database
contains the actual
password we want to send in the test.
Testing the Vulnerable Authentication
Given that the authorization middleware expects a database connection, testing it can be a messy exercise. To test it, we will have to open a database connection (to a test database) and then use it to initialize a test server that mounts the router. Then, we have to send requests to that same server and run our assertions on its response body and status.
Let’s take a stab at it:
We first open a database connection to a test database and pass the
connection as an argument to the
Router function. In the second highlighted
block, we mount the returned router from the
Router function on the test HTTP
server. Also, we create a client for the server that we will use to send
requests to the server.
We then build a new request for the server, and in the next highlighted block,
we attach the
username and the
password of the test case to the request
We send the request to the HTTP server using the
method, and get a response back.
We then parse the response and do our assertions on the status code of the
response and the contents of the body.
If we ran this test, it would fail. Why? Although the test setup is correct, the test database is empty. When we send a request to the HTTP server, it will try to validate the credentials our test sends, and it will find an empty database:
This means our test is missing an initial seed of test data, where we create
users table and put some records in it so we can use them in the test.
Adding data and cleaning it up, the old way
The widely-approved approach to seeding the test database is to insert some records before running the test and then remove them once the test is done. The benefit being that the database will always be empty after the tests are done running, without the need to recreate the database and its tables every time we run the tests. Precisely what our failing tests need.
Achieving this in the most idiomatic way is by using a cleanup closure that is returned by the function that inserts the data. Usually, the returning cleanup function “pattern” looks like this:
createUser function takes a database connection as an argument and uses
it to insert a new
User in the
users table. After that, it will return a
closure that, when invoked, will remove the added user.
The reason we pass the
testing.T pointer to the
createUser function is to
be able to fail the test if the database cannot be opened for writing. We check
db.Error field on the Gorm database connection pool for any errors and
t.Fatal function, if we hit any errors.
(You can read more about the behavior of
t.Fatal in my article on the
In the test itself, we invoke
createUser with the test database connection
testing.T pointer. The return value of
createUser is a function
which we store in the
cleanup variable. Then, we
defer the invocation of
cleanup to happen at the exit of the test function. Using
cleanup, we can
remove the data from the test database before exiting the tests.
While the approach above works and it’s idiomatic Go, it has a few downsides worth mentioning:
- It works fine for one test case, but if the test file has many tests that modify the state of the test database, they will get contaminated with cleanup-like functions.
- Adding to the above point, imagine tests where we have to clean up other things, such as processes, files, or open sockets. That will lead to a substantial increase in cleanup-closures pollution.
- It adds cognitive overhead when reading the code, which otherwise is a straightforward testing code
- The definition of the
cleanupfunction is buried in the
createUserfunction, visually separated from the test function that uses/invokes it. A reader of the test file can experience difficulty navigating the functions and putting the pieces together
Because of these reasons, the Go authors decided to add a
testing package. The
change was merged on
November 4, 2019 – right on time to get into the v1.14 release.
Let’s explore how
t.Cleanup can make our life easier.
Adding data and cleaning it up, using
b.Cleanup methods, we get better control to
cleaning up after our tests.
t.Cleanup registers a function to be called when
the test and all its subtests complete. This means that even if our tests run
as subtests, which is the case in our example, the cleanup will only happen
after all the subtests are done.
Here’s the reworked version of our tests:
While the code change is small, there is a substantial difference between the new and the old versions.
The most significant difference lies in the fact that the
takes care of its clean up now. The invoker function (
TestServer) does not
need to worry about the cleanup of the data added in the database – we know
createUser will remove the data. Most importantly, it will remove
the data just in time – once all of the subtests are done.
b.Cleanup in the standard library does not stop us
from using the old way using composition and returning cleanup callbacks – it
is still a valid way to clean up any state or files that our tests might
create. But, using the new
b.Cleanup functions things get a bit
easier: there’s now a way to do just that, and the standard library supports
And having the Go v1 compatibility promise in mind – it is here to stay.