Unit testing in Go

Go ships with the ‘testing’ package. Example:

package hello

import "testing"

func TestHelloWorld(t *testing.T) {
    expected := "Hello, World!"
    if observed := HelloWorld(); observed != expected {
        t.Fatalf("HelloWorld() = %v, want %v", observed, expected)
    }
}

Make sure to end your file name with _test.go. Read go help test for details.

Then run with:

$ go test

Or test a specific package:

$ go test hello

Or using the -v flag to also the list of tests descriptors:

$ go test -v
$ go test -v hello

Or to run tests that match a specific description, use -run <description>:

$ go test -v -cover -run 'Triangle'

Test single _test.go file

Go needs to compile .go files that are need for a given test file. For example, if we try to test builder_test.go, depending on how we run the test command, go fails to find the pieces of implementation used in the tests.

$ go test -v ./libhouse/builder_test.go
# command-line-arguments [command-line-arguments.test]
libhouse/builder_test.go:10:14: undefined: getBuilder
libhouse/builder_test.go:18:20: undefined: House
FAIL    command-line-arguments [build failed]
FAIL

But we can run tests in the entire libhouse package (as other necessary .go files will also be processed) and filter specific tests we want with -run TestName instead:

$ go test -v ./libhouse -run TestBuilder
=== RUN   TestBuilder
=== RUN   TestBuilder/returns_a_normal_builder
--- PASS: TestBuilder (0.00s)
    --- PASS: TestBuilder/returns_a_normal_builder (0.00s)
PASS
ok      main/libhouse   0.002s

A similar thing happens with go run. We may get those ‘undefined’ error messages unless we run a command in such a way that go run is able to process all the necessary files.

Subtests (or subcases) leaking state?

Consider testing a counter singleton with a test file like this one:

package singleton

import "testing"

func TestGetInstance(t *testing.T) {
    t.Run("create instance correctly", func(t *testing.T) {
        counter := GetInstance()

        if counter == nil {
            t.Error("Expected Singleton instance, got nil")
        }
    })

    t.Run("always returns he same reference", func(t *testing.T) {
        ref1 := GetInstance()
        ref2 := GetInstance()

        if ref1 != ref2 {
            t.Error("Counters are not the same reference to the same instance")
        }
    })

    t.Run("returns correct value on first call", func(t *testing.T) {
        count := GetInstance().AddOne()

        if count != 1 {
            t.Errorf("Want 1 for first call, got %d", count)
        }
    })

    t.Run("returns correct value on each subsequent call", func(t *testing.T) {
        counter := GetInstance()

        _ = counter.AddOne()
        valueForCall2 := counter.AddOne()

        if valueForCall2 != 2 {
            t.Errorf("Want 2 for second call, got %d", valueForCall2)
        }
    })
}

Basically, GetInstance() returns an instance of a counter singleton, and each time AddOne() is invoked on that instance, it increments and returns the next value of the counter.

But the last subtest fails because the value of the counter is 3, not 2!

And the problem is that the previous subtest invokes AddOne() once, and value of counter in the next subtest already starts at 1 (not 0).

This is not a problem with the singleton itself, which is supposed to be shared anyway (“Hey, that is what a singleton should do, right?”), but they way tests and subtests work in Go. The tests and subtests do not run like if they were individual programs (like in some other testing libraries in other languages). In the tests above, counter, ref1 and ref2 are all the same reference even though they are on their own blocks. So the counter we assigned in the first subtest creates an instance in memory which is shared with the other subtests.

Initially, we could think each test and each subtest should behave like its own, individual program, with its own memory allocation. But that is not the case. In reality, all tests of a single package behave like a single, individual program. And that is so true we can even define a TestMain for every individual package.

stretchr/testify package

https://github.com/stretchr/testify

Install or update:

$ go get github.com/stretchr/testify

From now on, we should have both assert and require available for importing in our test files:

package mypkg

import (
  "testing"
  "github.com/stretchr/testify/require"
  "github.com/stretchr/testify/assert"
)

Note

If we simply add the import (without manually installing the package with go get first), and then we run go mod tidy, the package will get installed and added to go.mod.

References