go / test assertions

is is a package that holds test assertions:

func TestRename(t *testing.T) {
	is := is.New(t)

	co, err := Rename(id, "Acme")

	is.NoErr(err)
	is.Eq(co.Name, "Acme")
}

A failure reads:

rename_test.go:9: co.Name = Beta, want Acme

The variable shadows the package on purpose, so a call site reads as English.

The label comes from the source

co.Name in that output is the source text of the assertion's first argument, read back from the test file. There is no message to write, so there is no message to drift.

The values always print. Only the label depends on reading the file, so a failed read degrades to got X, want Y. A literal argument gets no label, because []int{1, 2} = []int{1, 2} is noise.

Reading the source back is where an obvious implementation breaks. Under -trimpath, which I use to keep the test cache warm, runtime.Caller returns a module-relative path that will not open. Resolving filepath.Base does open, because go test runs each binary in its own package directory.

From there it parses the whole file and takes the call node covering the line, rather than the line's text, so a call spread over several lines keeps its label. The node has to be a call to the helper itself, so a same-named file from another package is rejected instead of misread.

gotest.tools/v3/assert has the same two ideas and repairs a relative path only when it detects a Bazel test, so under go test -trimpath its parse fails. matryer/is prints the trailing comment, which relocates a hand-written message rather than removing it.

Assertions

The name is the documentation:

New(t) fails fatally on the first failure. NewRelaxed(t) records and keeps going, for a test that reports every mismatch in a loop.

Use Nil rather than Eq(x, nil). A typed nil boxed into any is a non-nil interface, so Eq reports a nil *T as unequal to nil.

True takes an arbitrary bool because something has to: is.True(strings.Contains(s, x)) has no want to name. It is the escape hatch. Nothing stops True(got == want) from compiling, which would print false, want true and throw away both values. Only a vet pass would catch that.

Eq takes any

A method may not have type parameters, and these are methods, so Eq takes any. That gives up compile-time type checking:

is.Eq(gotInt64, 3) // builds, then fails: int64 3 against int 3

When the two values format identically the failure prints %T too, which is what makes such a call findable. Type the literal rather than converting the value under test.

Comparison uses reflect.Value.Equal where both values are comparable, which keeps =='s pointer identity, and reflect.DeepEqual where == is undefined, so slices and maps compare by contents.

← All articles