Writing Tests in Go

Opinions on how to write good tests, some controversial
Tests should be correct by inspection
Tests require a different approach than normal code. We don't have tests for tests, so tests need to be correct by inspection -- and the main technique to achieve this is to get rid of the generality of the production code, and exercise only very narrow and specific scenarios.

Decide what test to write

The goal of testing is to increase confidence
A test's purpose is to increase the confidence that you have in your program's correctness. The next test to write is the test that promises the highest ratio of additional confidence for the work required to write it.

Do test all your business decisions

Test all your code that is testable. Prioritize testing the code that (a) is important to work correctly (where confidence needs to be high) or (b) has become too complicated (where confidence has dropped too low).

Don’t test I/O and side effects

Controversial part ahead!
Sometimes, production code will do I/O or other side effects that are hard to simulate reproducably in a test. In these cases, it can sometimes be advisable to separate the I/O parts of the production code out and only test the rest - the bulk of your logic. It's key to only separate out only the smallest possible part that has the side effect, to keep as much as possible of the program testable.

“But G√ľnther”, I hear you saying, “what if there is a bug in these I/O parts”? Well - that’s why you need to keep them tiny and stare at them very long in a code review, to make sure they work without a test. It sounds heretic to leave this untested, but the alternative is to keep the I/O and other program logic together. That’ll easily make your more regular program logic hard to test, and that’s often not worth the trade off. After all, our goal is to increase confidence, not to reach full line coverage.

Special case: There are also some types of I/O which don’t affect your program logic, such as logging and incrementing performance counters. It’s ok to leave those untested without separating them out - these are battle proof APIs designed for quickly adding and removing them from your code, and they do not play a role for your programs correctness. It would give you little additional confidence to test them.

Structure your tests into Given/When/Then

Stick to the three test phases!

Stick with the setup/exercise/verify style of tests. Some people call this also Given/When/Then or the “Four Phase test” (counting tear-down as separate phase).

It does not matter what you call this pattern, but “Given/When/Then” tends to read nice in comments. Delimiting the section explicitly with comments is optional, but serves another useful purpose: You cannot use helper functions that span multiple of these purposes. (Those helper functions would be bad style.)

func TestSpellOut(t *testing.T) {
  // Given
  s := speller.New("en")

  // When
  got := s.SpellOut(42)

  // Then
  want := "forty-two"
  if got != want {
    t.Errorf("SpellOut(%q); got %q, want %q", 42, got, want)
  }
}

Test helpers

Test helper functions should be the primary way to share functionality between tests.

Each helper serves only one of the test phases
Make sure that each test helper helps with only one of the test phases: setup, exercise or verify.

In particular, resist the temptation to extract the whole test into a helper method, so that you can call it with differing parameters. The way this is done in Go instead is with table-driven tests and t.Run() if needed.1

Setup helpers

The checklist for setup helpers
A setup helper is called from a test's setup phase and should have the following properties:

This is a helper method for setting up a Speller object from the previous example:

func setUpSpeller(t testing.TB, path string) speller.Speller {
  t.Helper()

  s, err := speller.Load(path)
  if err != nil {
    t.Fatalf("Could not set up speller: speller.Load(%q): %v",
             path, err)
  }

  t.Cleanup(s.Close)
  return s
}

Exercise helpers

Don't.
Only introduce helpers for the test's execute phase if the invocation is unbearable to read.

It’s good practice to exercise the component under test directly without such a helper. Using the component directly is a good test for its API’s usability and will often uncover small possibilities for API improvements in the process.

Verification helpers

Alternatives to assertion libraries
Common Go style discourages the use of assertion helper libraries, which are common in most other languages.

While I don’t necessarily agree with that, the next best thing you can do within the bounds of this style is to extract helpers that do the necessary comparisons for you, if necessary. The ground rule I use is: The if should be part of the top-level test function, but the condition in that check can call the helper.

General advice on writing tests

A lot of general advice from other testing frameworks is applicable to Go too.

Make each test exercise a specific narrow scenario

Each test should exercise a specific and narrow scenario. There should be little overlap between tests. Any bug introduced in the code should ideally only break a single test, which has narrow focus and is related to the broken functionality.

Start with the assertion

Start writing tests with the assertion, and work from there upwards by filling out the missing variables that aren’t defined yet. This approach helps me to focus each test around one specific and narrow behaviour that I want to test.

One test for the happy path, plus one for each error

With simple functions with inputs, outputs and errors, I like to have one main test for the happy path, plus one for each error, whose test input is based on the one for the happy path, with a modification.

See my article on Shared base fixtures for examples.

Avoid TestMain, use setup helper functions

If you don’t know what TestMain is, good!

TestMain is a way to share common test setup and teardown between multiple test cases, without them calling a setup helper explicitly. In most cases, this is better done with a setup helper function as described above.

There is one case where TestMain is acceptable, which is the case where the test setup is so expensive to run that it will be significantly faster to only do it once.


  1. t.Run() is a way of structuring your test output, but it’ll also make sure that subtests can cancel with t.Fatal(), without affecting the execution of other subtests. ↩︎

  2. testing.TB is an interface for the common methods in testing.T and testing.B, so it can be used from tests and benchmarks alike. ↩︎

Comments