Helpers to complement Go testing package.
Write tests with ease and fun!
Plain Go tests force a choice between two extremes:
verbose if got != want { t.Fatalf(...) } boilerplate for every comparison,
or reaching for a heavier test framework
that replaces go test with its own runner and vocabulary.
This project avoids both:
- It never introduces a new concept - no suites, no custom runner, no dot-import.
Every wrapped value is still a real
*testing.T/*testing.B/*testing.Funderneath, sott.Run,tt.Parallel,t.Log,t.Cleanupand everything else you already know keep working exactly as before. - Checkers already know what "nil", "equal" or "contains" means for the type you handed them,
so you rarely write the type-specific comparison yourself:
t.Nil(err)handles typed-nil pointers correctly (see theNildoc comment for the classic gotcha),t.Len(v, 3)works the same for a map, slice, string, channel or array,t.Err(err, io.EOF)unwraps and compares by value instead of just by chain membership. - A failed check prints a full, readable dump of both values plus a text diff,
so
t.DeepEqual(got, want)on a whole struct/slice/map actually shows you what's wrong, instead of thegot X, want Yyou'd hand-roll around==. See Failure Output below. check.Must()/check.New()let you pick, per test, whether a failed check should stop the test (testify/require-like) or just record the failure and continue (testify/assert-like), without importing two different packages for that.check.TestMainadds a per-test and grand-total pass/fail/todo counter to every run, so you get a sense of overall test health beyond individual PASS/FAIL lines - see thechecks:lines in Failure Output.
Note
The first check commit dates back to December 2017 —
a time when testify
had seen no activity for five months, with 40+ pull requests piling up
and the organization's domain parked for sale
(issue #526).
testify has since recovered, but check was born from the uncertainty
of that period and took a different approach from the start.
- Zero required external dependencies. Protobuf/gRPC comparison support pulls its (much heavier) dependencies in only if you opt into the companion submodules.
- Compelling output from failed tests:
- Very easy-to-read dumps for expected and actual values.
- Same text diff you loved in testify.
- Statistics with amount of passed/failed checks.
- Colored output in terminal.
- 100% compatible with testing package - check package just provides convenient wrappers
for
*testing.T/*testing.B/*testing.Fmethods without an unusual execution flow (see Non-goals). - All checks you may ever need! :)
- Very easy to add your own check functions.
- Concise, handy and consistent API, without dot-import!
Wrap each (including subtests) *testing.T/*testing.B/*testing.F
using check.Must() and write tests as usually with testing package.
Call new methods provided by this package to have more clean/concise test code
and cool dump/diff.
check.Must() is the recommended default: it stops the test on the first failed check
(like testify/require). Use check.New() instead for the softer,
testify/assert-like behavior where a failed check doesn't stop the test.
Note
Call tb.Run()/tb.Parallel() on the original *testing.T/*testing.B
before wrapping it with check.Must()/check.New() —
these two aren't available on the wrapped value
(this also satisfies the paralleltest linter):
import "github.com/powerman/check"
func TestSomething(tt *testing.T) {
tt.Parallel()
t := check.Must(tt)
t.Equal(2, 2)
t.Log("You can use new t just like usual *testing.T")
tt.Run("Subtests/Parallel example", func(tt *testing.T) {
tt.Parallel()
t := check.Must(tt)
t.NotEqual(2, 3, "should not be 3!")
obj, err := NewObj()
if t.Nil(err) {
t.Match(obj.field, `^\d+$`)
}
})
}To get optional statistics about executed checkers add:
func TestMain(m *testing.M) { check.TestMain(m) }See the package examples
for more runnable snippets: table-driven subtests, soft checks with New, TODO,
custom Should checkers, Err/ErrIs/ErrAs/Match side by side, and MergeContext.
check.T(tt *testing.T) *check.C is the original, soft-mode by default constructor
kept for backward compatibility - *check.C behaves exactly like it always did,
including direct access to the wrapped *testing.T via its T field.
New code should prefer check.New()/check.Must().
go get github.com/powerman/checkHere's what a failed DeepEqual looks like (from testdata/demo/demo_test.go,
run with go test -tags demo -v ./testdata/demo/).
Only Total actually differs - ID, Customer and Items match -
so Diff singles out the one line that's wrong
instead of making you eyeball two full dumps for what changed.
The checks: lines at the end are check.TestMain's pass/fail/todo counters
(one passing Equal plus this failing DeepEqual):
=== RUN TestDemoFailure
demo_test.go:41: order total should match after checkout
Checker: DeepEqual
Expected: (demo.Order) {
ID: (string) (len=3) "A-1",
Customer: (string) (len=3) "Ann",
Total: (int) 40,
Items: ([]string) (len=2) {
(string) (len=3) "pen",
(string) (len=3) "cup"
}
}
Actual: (demo.Order) {
ID: (string) (len=3) "A-1",
Customer: (string) (len=3) "Ann",
Total: (int) 42,
Items: ([]string) (len=2) {
(string) (len=3) "pen",
(string) (len=3) "cup"
}
}
Diff:
--- Expected
+++ Actual
@@ -3,3 +3,3 @@
Customer: (string) (len=3) "Ann",
- Total: (int) 40,
+ Total: (int) 42,
Items: ([]string) (len=2) {
--- FAIL: TestDemoFailure (0.00s)
checks: 1 passed 1 failed TestDemoFailure
checks: 1 passed 0 todo 1 failed (total)
With a color terminal (see doc.go for the FORCE_COLOR/NO_COLOR environment variables)
the same failure looks like this:
You can extend DeepEqual/NotDeepEqual and Err/NotErr
with custom comparison logic via RegisterEqualChecker and RegisterErrChecker.
- This package enables validator
FieldErrorand[]FieldErrorcomparison byNamespace()+Tag()viacheck.Err/check.NotErr.
Protobuf message comparison and gRPC status error comparison have been extracted into separate modules to keep the core dependency-light:
- checkproto — enables
proto.Equalviacheck.DeepEqual/check.NotDeepEqualfor protobuf messages. - checkgrpc — enables
gRPC status comparison via
check.Err/check.NotErr. It also imports checkproto, so a single blank import covers both.
Usage: just add a blank import in your test file or TestMain:
import _ "github.com/powerman/checkgrpc"A few honest notes on how check compares to other assertion libraries, so you can pick the right one instead of the one you found first.
vs testify
testify's assert/require packages solve the same problem:
convenience wrappers around *testing.T with a soft/hard mode split,
and (via assert.New(t)/require.New(t)) a method-style API too,
so that's not a real difference. Where check does differ:
- Argument order: check and go-quicktest/qt put the actual value first,
matching
if got != want(t.Equal(got, want)) and thegot = %v, want %vshape of Go's own idiomatic test failure messages. testify and shoenig/test put the expected value first instead (assert.Equal(t, want, got),must.Eq(t, want, got)), which only really makes sense if you're already used to it:gotis usually a separate variable set by calling the code under test, often on its own long line, whilewantis often a short literal written inline -want, gotorder buries the value to check at the end of the call. - Dump + diff is the default failure output, not an opt-in - you don't need
assert.EqualExportedValuesor a separate diff helper to get a readable struct comparison. - No suite package, no mock package - check stays a pure assertion/checker library.
Use
testify/mock(or anything else) alongside it if you need mocks.
A few UX differences side by side:
- Error is nil:
t.Nil(err)vsassert.NoError(t, err)orassert.Nil(t, err)- two names for one idea. - Error unwraps to & value-equals a sentinel/custom error:
t.Err(err, io.EOF)vs no direct equivalent -assert.ErrorIsonly checks chain membership, not value equality of a freshly-constructed error. - Length of a map/slice/string/channel:
t.Len(v, 3)vsassert.Len(t, v, 3), butlen == 0needs the separateassert.Empty/NotEmpty.
check does not cover 100% of testify's assert/require surface.
Beyond Eventually/Never and the HTTP handler helpers
(deliberately left out, see Non-goals),
testify's IsIncreasing/IsDecreasing/IsNonIncreasing/IsNonDecreasing
(sequence monotonicity) and YAMLEq have no check equivalent - not deliberately,
just not needed yet. (Open an issue if you need them.)
Everything else (Same, EqualValues, Positive, Negative, ...)
is one line via an existing checker
(t.Equal already compares pointers by identity, t.Greater(x, 0) covers Positive).
vs go-quicktest/qt and shoenig/test
go-quicktest/qt and shoenig/test are newer, generics-first libraries:
qt.Assert(t, got, qt.Equals(want)) and must.Eq(t, want, got)
are package-level generic functions, so a type mistake (comparing int to int32)
is often a compile error instead of a runtime panic or failed check.
In practice that payoff is smaller than it sounds: a type mistake fails the test either way,
the only question is whether you see it as a go build/go vet error
before running go test (qt/shoenig/test) or as a panic/failed check while it runs (check) -
not whether it's caught.
check is deliberately not generic for a more concrete reason:
part of its convenience only works with any
(Match accepts string/[]byte/error/fmt.Stringer; Equal special-cases time.Time),
and Go doesn't yet support generic methods,
so a generic check.TB isn't possible without splitting the API into free functions.
Pick qt or shoenig/test if you want compile-time typed assertions as package functions;
pick check if you want a method-style API and check's dump/diff by default.
gotest.tools/v3/assert is close in spirit
(wraps *testing.T-style helpers, diffs with go-cmp)
but keeps the package-function shape
(assert.Equal(t, x, y), assert.DeepEqual(t, x, y, opts...))
and leans on go-cmp's option system for custom comparisons,
where check uses a small RegisterEqualChecker/RegisterErrChecker registry instead.
If you already rely on go-cmp's options (unexported fields, custom comparers, ...),
it plugs into check just as easily: register it once with
check.RegisterEqualChecker(func(a, b any) (bool, bool) { return cmp.Equal(a, b), true })
to make it check's default, or call it ad hoc with t.True(cmp.Equal(got, want))
(no dump/diff for that one call, but full access to go-cmp's own options).
- No BDD/suites - check stays inside
go test,t.Runandt.Parallel; see Rationale. - No
Eventually/Neverpolling helpers - Go'stesting/synctestcovers that class of test better (deterministic virtual time, no flaky sleeps). - No HTTP handler assertions (testify's
HTTPSuccess/HTTPRedirect/HTTPBodyContains/...) -net/http/httptestplus check's own checkers already cover that ground, e.g.t.Match(rec.Body.String(), pattern)ort.Equal(rec.Code, http.StatusOK). - No mocking - pair check with whatever mocking library you already use.
