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:
Eq,NotEqfor comparing two values. The default.NoErr,HasErrfor errors.Nil,NotNilfor nil checks.Truefor a predicate with no want to name.
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.