Test assertions for Go, defined by a language-neutral standard and held to it on every run.
import "go.dokimi.dev/assert"
func TestGet(t *testing.T) {
item, err := store.Get(ctx, id)
assert.NoError(t, err, "Get succeeds for a key that is present")
assert.Equal(t, item, want, "Get returns the stored item")
}Every assertion takes a message last. It states the contract under test and is the first line of the failure, so a failure says what was supposed to be true rather than only what was observed:
Get returns the stored item: (-want +got)
store.Item{
ID: "abc",
- Name: "widget",
+ Name: "wigdet",
}
go get go.dokimi.dev/assertRequires Go 1.27. One dependency: github.com/google/go-cmp.
assert stops the test at the first failure. expect records the
failure and carries on, for when several properties of one value are
each worth seeing:
expect.That(t, user).
NotNil("the user was found").
HasPrefix("usr_", "the id carries its prefix").
Length(3, "every field was populated")One run reports all three. Both packages carry the same assertions under the same names and share one comparison; a conformance test fails the build if they ever diverge.
Every assertion exists as a function and, where its first argument is the value being examined, as a chain method:
assert.Equal(t, got, want, "the values match")
assert.That(t, got).Equal(want, "the values match")The rules most libraries leave to their comparison library's defaults, stated instead:
| Rule | Reverse it with |
|---|---|
| A nil map or slice does not equal an empty one | EquateEmpty() |
| NaN does not equal NaN | EquateNaNs() |
| Floats compare exactly | CloseTo applies a tolerance |
| Unexported fields take part | — |
| Values of different types never compare | — |
The first is the one that catches people. []int(nil) and []int{}
are different answers, and a test may need to tell them apart. The
same assertions exist in Python, PHP and TypeScript, where None,
null and undefined are distinct from an empty list; equating them
everywhere would make those libraries report values they were never
given.
An option applies to the call it is passed to and to nothing else.
A check whose every statement is NoError passes against a subject
whose methods do nothing and return nil. It reads as coverage and
establishes nothing.
Rejects drives a check against an implementation it is meant to
reject, and fails when the check passes:
got := assert.Rejects(t, "a store that overwrites fails the check",
func(tb assert.TB) { refusesADuplicate(tb, overwritingStore{}) })
assert.Contains(t, got, "the key was already present",
"and fails for the reason the check is about")Assert on the returned message. A subject that panics before reaching the assertion satisfies a bare call while the check's own assertion never ran.
| Import | What it holds |
|---|---|
go.dokimi.dev/assert |
34 assertions and a 15-method chain, stopping at the first failure |
go.dokimi.dev/assert/expect |
the same, recording and continuing |
go.dokimi.dev/assert/golden |
comparison against a recorded file, with scrubbers for content that changes each run |
go.dokimi.dev/assert/bench |
ceilings on latency, allocations and bytes per benchmark iteration |
go.dokimi.dev/assert/conformance |
this library checked against the standard |
golden.Match(t, "response.json", body, golden.ShouldUpdate(),
golden.ScrubTimestamps())go test -update rewrites the file. Read the diff before you do: a
golden file updated without reading it records whatever the code now
does, which is the opposite of an assertion.
Scrubbers replace content that differs between runs, on both sides of
the comparison, so the parts that should be stable are the parts
compared. ScrubTimestamps, ScrubHashes, ScrubRunIDs and
ScrubJSONFields are supplied; a Scrubber is a func(string) string.
A benchmark records numbers; somebody has to read them to notice a regression. A contract states the ceiling in the benchmark, so exceeding it fails the build:
func BenchmarkGet(b *testing.B) {
c := bench.Start(b).MaxLatency(50 * time.Microsecond).MaxAllocs(2)
defer c.End()
for c.Loop() {
_, _ = store.Get(ctx, id)
}
}Ceilings are checked together, so one run names each one exceeded. The p99 rather than the mean, because the tail is what a caller waits for.
| Name | What it states |
|---|---|
Equal |
Structural equality. A null collection does not equal an empty one, no type coercion, NaN is unequal to itself, floats compare exactly, cycles stop, functions compare by identity. |
NotEqual |
Negation of equal. |
True |
The condition holds. The failure carries the caller's message alone. |
False |
The condition does not hold. |
Nil |
The value is absent. A typed nil counts as nil. |
NotNil |
The value is present. A typed nil counts as nil. |
Length |
The container holds the stated number of items. Answers for any sized container or text. |
Empty |
The container holds nothing. |
NotEmpty |
The container holds something. |
| Name | What it states |
|---|---|
Contains |
Text holds a substring, a sequence holds an element, or a map holds a key. |
NotContains |
Negation of contains. |
ContainsInOrder |
Text holds every needle, each after the previous one's match ends. |
HasPrefix |
Text starts with the given prefix. |
HasSuffix |
Text ends with the given suffix. |
Matches |
Text matches a regular expression. A pattern that does not compile is a failure, not an error. |
| Name | What it states |
|---|---|
CloseTo |
A number is within a tolerance of another, by absolute difference. NaN is outside every tolerance. |
InRange |
A number falls in a closed interval. NaN is in no range. |
Pairwise |
Every adjacent pair of a sequence satisfies a predicate. Nought or one item passes. |
| Name | What it states |
|---|---|
NoError |
No failure value is present. |
HasError |
Some failure value is present. |
ErrorIs |
A failure matches a sentinel, through the chain of wrapped causes. |
ErrorIsNot |
A failure does not match a sentinel. |
ErrorAs |
A failure of the given type is in the chain. Yields it. |
Panics |
A callable raises. Yields what was raised. |
NotPanics |
A callable does not raise. |
| Name | What it states |
|---|---|
HonoursCancellation |
A subject given a cancelled handle reports a cancellation failure. |
HonoursDeadline |
A subject given an expired deadline reports a deadline failure. |
CompletesWithin |
A subject finishes before the stated duration. |
Pure |
Observed state is unchanged across a call. |
NilContextSafe |
A subject given an absent cancellation handle does not crash. |
| Name | What it states |
|---|---|
Eventually |
An assertion body passes within a timeout, retried at an interval. Reports the last failure. |
EventuallyTrue |
A predicate becomes true within a timeout, retried with backoff. |
NoGoroutineLeaks |
No concurrent task started in the scope outlives it. |
| Name | What it states |
|---|---|
golden.Match |
Output matches the golden file resolved against the conventional directory. |
golden.MatchAt |
Output matches the golden file at a given path. |
golden.MatchJSONField |
Output matches one named field of a golden JSON object. |
| Name | What it states |
|---|---|
bench.Contract.MaxLatency |
The p99 latency per iteration stays within a ceiling. |
bench.Contract.MaxMean |
The mean latency per iteration stays within a ceiling. |
bench.Contract.MaxAllocs |
The allocations per iteration stay within a ceiling. |
bench.Contract.MaxBytes |
The bytes allocated per iteration stay within a ceiling. |
| Name | What it states |
|---|---|
Rejects |
A check fails against an implementation it is meant to reject. Yields the failure message. |
The assertions are defined in assert-spec, language-neutral, and
implemented in six languages. This library vendors the definition and
holds itself to it on every run:
- Completeness. Every assertion is present under the name the definition gives it, or declared absent with a stated reason. An undeclared absence fails the build; so does a declared absence for something that is implemented.
- Parity. Both surfaces carry the same members.
- Meaning. 87 corpus cases state what an assertion must report, shared with every other implementation.
The corpus reaches 25 of the 41. A case states its arguments as data, so it cannot cover an assertion that takes a callable, a cancellation handle, a golden file or a benchmark. Those are checked for presence and tested here.
make check # the full pre-merge gate
make test # tests
make lint # vet, golangci-lint, markdown, licence headers, vulnerabilities
make spec-sync # refresh the vendored definitionMIT. See LICENSE.