I have a lot of tests in Go that integrate with Postgres, and test the interactions between Go models and the database.
A lot of these tests can run in parallel. For example, any test that attempts to write a record, but fails with a constraint failure, can run in parallel with all other tests. A test that tries to read a random database ID and expects to not fetch a record can run in parallel with other tests. If you write your tests so they all use random UUID's, or all run inside of transactions, you can run them in parallel. You can use this technique to keep your test suite pretty fast, even if each individual test takes 20-40 milliseconds.
You can mark a test to run in parallel by calling t.Parallel()
at the top of
the test. Here's an example test from the job queue Rickover:
func TestCreateMissingFields(t *testing.T) { t.Parallel() test.SetUp(t) job := models.Job{ Name: "email-signup", } _, err := jobs.Create(job) test.AssertError(t, err, "") test.AssertEquals(t, err.Error(), "Invalid delivery_strategy: \"\"") }
This test will run in parallel with other tests marked Parallel
and only
with other tests marked Parallel; all other tests run sequentially.
The problem comes when you want to clear the database. If you have a
t.Parallel()
test clean up after it has made its assertions, it might try
to clear the database while another Parallel() test is still running! That
wouldn't be good at all. Presumably, the sequential tests are expecting the
database to be cleared. (They could clear it at the start of the test, but this
might lead to unnecessary extra DB writes; it's better for tests that alter the
database to clean up after themselves).
(You can also run every test in a transaction, and roll it back at the end.
Which is great, and gives you automatic isolation! But you have to pass
a *sql.Tx
around everywhere, and make two additional roundtrips to the
database, which you probably also need to do in your application).
Go 1.7 adds the ability to nest tests. Which means we can run setup once, run every parallel test, then tear down once. Something like this (from the docs):
func TestTeardownParallel(t *testing.T) { // This Run will not return until the parallel tests finish. t.Run("group", func(t *testing.T) { t.Run("Test1", parallelTest1) t.Run("Test2", parallelTest2) t.Run("Test3", parallelTest3) }) // <tear-down code> }
Note you have to lowercase the function names for the parallel tests, or they'll run inside of the test block, and then again, individually. I settled on this pattern:
var parallelTests = []func(*testing.T){ testCreate, testCreateEmptyPriority, testUniqueFailure, testGet, } func TestAll(t *testing.T) { test.SetUp(t) defer test.TearDown(t) t.Run("Parallel", func(t *testing.T) { for _, parallelTest := range parallelTests { test.F(t, parallelTest) } }) }
The test
mentioned there is the set of test helpers from the Let's Encrypt
project, plus some of my own. test.F
finds the defined function
name, capitalizes it, and passes the result to test.Run:
// capitalize the first letter in the string func capitalize(s string) string { r, size := utf8.DecodeRuneInString(s) return fmt.Sprintf("%c", unicode.ToTitle(r)) + s[size:] } func F(t *testing.T, f func(*testing.T)) { longfuncname := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name() funcnameparts := strings.Split(longfuncname, ".") funcname := funcnameparts[len(funcnameparts)-1] t.Run(capitalize(funcname), f) }
The result is a set of parallel tests that run a cleanup action exactly once.
The downside is the resulting tests have two levels of nesting; you have to
define a second t.Run
that waits for the parallel tests to complete.
=== RUN TestAll
=== RUN TestAll/Parallel
=== RUN TestAll/Parallel/TestCreate
=== RUN TestAll/Parallel/TestCreateEmptyPriority
=== RUN TestAll/Parallel/TestUniqueFailure
=== RUN TestAll/Parallel/TestGet
--- PASS: TestAll (0.03s)
--- PASS: TestAll/Parallel (0.00s)
--- PASS: TestAll/Parallel/TestCreate (0.01s)
--- PASS: TestAll/Parallel/TestCreateEmptyPriority (0.01s)
--- PASS: TestAll/Parallel/TestUniqueFailure (0.01s)
--- PASS: TestAll/Parallel/TestGet (0.02s)
The other thing that might trip you up: If you add print statements to your
tear down lines, they'll appear in the console output before the PASS
lines.
However, I verified they run after all of your parallel tests are finished
running.
Liked what you read? I am available for hire.