Go Subtest Tips

I recently learned more detail about subtests in Go.

Testing in Go is a first class concept. It’s this easy (example taken from play.golang.org:

package main

import (
	"testing"
)

// LastIndex returns the index of the last instance of x in list, or
// -1 if x is not present. The loop condition has a fault that
// causes somes tests to fail. Change it to i >= 0 to see them pass.
func LastIndex(list []int, x int) int {
	for i := len(list) - 1; i > 0; i-- {
		if list[i] == x {
			return i
		}
	}
	return -1
}

func TestLastIndex(t *testing.T) {
	tests := []struct {
		list []int
		x    int
		want int
	}{
		{list: []int{1}, x: 1, want: 0},
		{list: []int{1, 1}, x: 1, want: 1},
		{list: []int{2, 1}, x: 2, want: 0},
		{list: []int{1, 2, 1, 1}, x: 2, want: 1},
		{list: []int{1, 1, 1, 2, 2, 1}, x: 3, want: -1},
		{list: []int{3, 1, 2, 2, 1, 1}, x: 3, want: 0},
	}
	for _, tt := range tests {
		if got := LastIndex(tt.list, tt.x); got != tt.want {
			t.Errorf("LastIndex(%v, %v) = %v, want %v", tt.list, tt.x, got, tt.want)
		}
	}
}

(Try this at home.)

The output of the above is:

=== RUN   TestLastIndex
    TestLastIndex: prog.go:34: LastIndex([1], 1) = -1, want 0
    TestLastIndex: prog.go:34: LastIndex([2 1], 2) = -1, want 0
    TestLastIndex: prog.go:34: LastIndex([3 1 2 2 1 1], 3) = -1, want 0
--- FAIL: TestLastIndex (0.00s)
FAIL

1 test failed.

Tests of any substance I tend to break up into subtests. I might change the above like this:

package main

import (
	"strconv"
	"testing"
)

func LastIndex(list []int, x int) int {
	for i := len(list) - 1; i > 0; i-- {
		if list[i] == x {
			return i
		}
	}
	return -1
}

func TestLastIndex(t *testing.T) {
	tests := []struct {
		list []int
		x    int
		want int
	}{
		0: {list: []int{1}, x: 1, want: 0},
		1: {list: []int{1, 1}, x: 1, want: 1},
		2: {list: []int{2, 1}, x: 2, want: 0},
		3: {list: []int{1, 2, 1, 1}, x: 2, want: 1},
		4: {list: []int{1, 1, 1, 2, 2, 1}, x: 3, want: -1},
		5: {list: []int{3, 1, 2, 2, 1, 1}, x: 3, want: 0},
	}
	for i, tt := range tests {
		t.Run(strconv.Itoa(i), func(t *testing.T) {
			if got := LastIndex(tt.list, tt.x); got != tt.want {
				t.Errorf("LastIndex(%v, %v) = %v, want %v", tt.list, tt.x, got, tt.want)
			}
		})
	}
}

(Try this at home.)

The output of this version is:

=== RUN   TestLastIndex
=== RUN   TestLastIndex/0
    TestLastIndex/0: prog.go:33: LastIndex([1], 1) = -1, want 0
=== RUN   TestLastIndex/1
=== RUN   TestLastIndex/2
    TestLastIndex/2: prog.go:33: LastIndex([2 1], 2) = -1, want 0
=== RUN   TestLastIndex/3
=== RUN   TestLastIndex/4
=== RUN   TestLastIndex/5
    TestLastIndex/5: prog.go:33: LastIndex([3 1 2 2 1 1], 3) = -1, want 0
--- FAIL: TestLastIndex (0.00s)
    --- FAIL: TestLastIndex/0 (0.00s)
    --- PASS: TestLastIndex/1 (0.00s)
    --- FAIL: TestLastIndex/2 (0.00s)
    --- PASS: TestLastIndex/3 (0.00s)
    --- PASS: TestLastIndex/4 (0.00s)
    --- FAIL: TestLastIndex/5 (0.00s)
FAIL

4 tests failed.

There are two nice thing about subtests:

  • built in reporting of which tests failed (0, 2, and 5)
  • the ability to only run some subset of tests

The built in test framework allows running tests like this:

$ go test ./...

That runs tests for all of the go recursively under the current directory. Sometimes when you are dealing with a failing test you might want to run just the broken ones:

$ go test -v -run 'Last/[025]'
=== RUN   TestLastIndex
=== RUN   TestLastIndex/0
    TestLastIndex/0: x_test.go:33: LastIndex([1], 1) = -1, want 0
=== RUN   TestLastIndex/2
    TestLastIndex/2: x_test.go:33: LastIndex([2 1], 2) = -1, want 0
=== RUN   TestLastIndex/5
    TestLastIndex/5: x_test.go:33: LastIndex([3 1 2 2 1 1], 3) = -1, want 0
--- FAIL: TestLastIndex (0.00s)
    --- FAIL: TestLastIndex/0 (0.00s)
    --- FAIL: TestLastIndex/2 (0.00s)
    --- FAIL: TestLastIndex/5 (0.00s)
FAIL
FAIL    x       0.003s
FAIL

The -run flag to go test is a regex, or actually a bunch of regexen separated with /s. This is super handy and allows for realy brief commandlines, but also there are some subtle implications. Consider this totally contrived code:

package main

import (
	"testing"
)

func LastIndex(list []int, x int) int {
	for i := len(list) - 1; i > 0; i-- {
		if list[i] == x {
			return i
		}
	}
	return -1
}

func TestLastIndex(t *testing.T) {
	tests := []struct {
		name string
		list []int
		x    int
		want int
	}{
		{name: "single", list: []int{1}, x: 1, want: 0},
		{name: "pair", list: []int{1, 1}, x: 1, want: 1},
		{name: "pair/different", list: []int{2, 1}, x: 2, want: 0},
		{name: "four", list: []int{1, 2, 1, 1}, x: 2, want: 1},
		{name: "five", list: []int{1, 1, 1, 2, 2, 1}, x: 3, want: -1},
		{name: "six", list: []int{3, 1, 2, 2, 1, 1}, x: 3, want: 0},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := LastIndex(tt.list, tt.x); got != tt.want {
				t.Errorf("LastIndex(%v, %v) = %v, want %v", tt.list, tt.x, got, tt.want)
			}
		})
	}
}

(Try at home.)

Output is (hopefully unsurprisingly:)

=== RUN   TestLastIndex
=== RUN   TestLastIndex/single
    TestLastIndex/single: prog.go:33: LastIndex([1], 1) = -1, want 0
=== RUN   TestLastIndex/pair
=== RUN   TestLastIndex/pair/different
    TestLastIndex/pair/different: prog.go:33: LastIndex([2 1], 2) = -1, want 0
=== RUN   TestLastIndex/four
=== RUN   TestLastIndex/five
=== RUN   TestLastIndex/six
    TestLastIndex/six: prog.go:33: LastIndex([3 1 2 2 1 1], 3) = -1, want 0
--- FAIL: TestLastIndex (0.00s)
    --- FAIL: TestLastIndex/single (0.00s)
    --- PASS: TestLastIndex/pair (0.00s)
    --- FAIL: TestLastIndex/pair/different (0.00s)
    --- PASS: TestLastIndex/four (0.00s)
    --- PASS: TestLastIndex/five (0.00s)
    --- FAIL: TestLastIndex/six (0.00s)
FAIL

4 tests failed.

Now let’s say you wanted to run just pair/different while you debug it. In this test there’s not a lot of output, but sometimes tests might have tons of lines of output so it can be useful to only run what you must. Here’s some commands you might consider trying to run pair/different:

  • go test -run Index/pair/different
  • go test -run Index/pair\/different
  • go test -run 'Index/pair\/different'
  • go test -run 'Index/pair[/]different'
  • go test -run 'Index/pair.different'

All of these either run both pair and pair/different or they run no subtests at all. The answer comes in the documentation for -run:

        -run regexp                            
            Run only those tests and examples matching the regular expression.
            For tests, the regular expression is split by unbracketed slash (/)
            characters into a sequence of regular expressions, and each part                   
            of a test's identifier must match the corresponding element in                     
            the sequence, if any. Note that possible parents of matches are
            run too, so that -run=X/Y matches and runs and reports the result   
            of all tests matching X, even those without sub-tests matching Y,                  
            because it must run them to look for those sub-tests.  

This explains both of the outcomes in the commands above. When you run go test -run X/Y/Z go creates three regexp objects (one for X, one for Y, and one for Z.) It matches X against the function name of the test; if it matches, it will then run the test. When the test is running, it tries to run all subtests and if the subtest matches Y, the subtest gets run. Finally any subtests of Y named Z will be run, though we happen to not have a third level of nesting here.

This implies two rules for subtests in go. First, do not put / in subtest names. One could argue that testing should actually escape these, but just not using / is simple. If you have a / in a subtest name and the prefix before the matches other tests, you’ll never be able to run the longer named test without also running the prefix. At work I used to have tests that had filenames in the subtest name; because of this I replaced the / with something like !.

Second, if possible, keep the meat of the test in the leaf tests. Anything in non-leaf tests may get run and may produce distracting output.


(Affiliate links below.)

If you want to learn more about programming Go, you should check out The Go Programming Language. It is one of the best programming books I’ve read. You will not only learn Go, but also get some solid introductions on how to write code that is safely concurrent. Highly recommend.

Another book to consider learning Go with is Go Programming Blueprints. It has a nearly interactive style where you write code, see it get syntax errors (or whatever,) fix it, and iterate. A useful book that shows that you don’t have to get all of your programs perfectly working on the first compile.

Posted Tue, May 5, 2020

If you're interested in being notified when new posts are published, you can subscribe here; you'll get an email once a week at the most.