The Best Go Assertion and Mocking Libraries Compared
Compare Go assertion libraries by the failure each prints for one wrong slice element, then see where mocks fit: stdlib, go-cmp, testify and more.

Pick a Go assertion library by what it prints when a test fails, because that is the only moment it matters. When everything passes, all the options behave identically. When something breaks, you are reading the message at the worst time, and the library that tells you what differs, where, in a form you can act on, has earned its place.
Tested with Go 1.25.6, testify v1.11.1, go-cmp v0.7.0, gotest.tools v3.5.2, matryer/is v1.4.1, macOS on Apple Silicon (arm64)
This roundup runs five approaches against the same failing comparison on Go 1.25.6, shows the output of each, and then covers mocking libraries, where the choice is about what you generate and how much of your tests become coupled to it.
The experiment
package main
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/matryer/is"
"github.com/stretchr/testify/assert"
gtassert "gotest.tools/v3/assert"
)
type User struct {
Name string
Roles []string
}
var got = User{"ada", []string{"admin", "dev"}}
var want = User{"ada", []string{"admin", "ops"}}
func TestStdlib(t *testing.T) {
if !equal(got, want) {
t.Errorf("got %+v, want %+v", got, want)
}
}
func TestCmp(t *testing.T) {
if d := cmp.Diff(want, got); d != "" {
t.Errorf("mismatch (-want +got):\n%s", d)
}
}
func TestTestify(t *testing.T) { assert.Equal(t, want, got) }
func TestGotestTools(t *testing.T) { gtassert.DeepEqual(t, got, want) }
func TestMatryerIs(t *testing.T) { is.New(t).Equal(got.Roles[1], want.Roles[1]) }
func equal(a, b User) bool {
return a.Name == b.Name && len(a.Roles) == len(b.Roles) && a.Roles[1] == b.Roles[1]
}
func main() {}for t in TestStdlib TestCmp TestTestify TestGotestTools TestMatryerIs; do
echo "=== $t"
go test -count=1 -run "^$t\$" . 2>&1 | grep -vE '^(--- FAIL|FAIL|exit status|\s*$)' | sed -E 's/\x1b\[[0-9;]*m//g' | sed -E 's#^ +[a-z_]+\.go:[0-9]+: ?##; s#/[^ ]*/cmp_test.go#cmp_test.go#' | head -14
done=== TestStdlib
got {Name:ada Roles:[admin dev]}, want {Name:ada Roles:[admin ops]}
=== TestCmp
mismatch (-want +got):
main.User{
Name: "ada",
Roles: []string{
"admin",
- "ops",
+ "dev",
},
}
=== TestTestify
Error Trace: cmp_test.go:30
Error: Not equal:
expected: main.User{Name:"ada", Roles:[]string{"admin", "ops"}}
actual : main.User{Name:"ada", Roles:[]string{"admin", "dev"}}
Diff:
--- Expected
+++ Actual
@@ -4,3 +4,3 @@
(string) (len=5) "admin",
- (string) (len=3) "ops"
+ (string) (len=3) "dev"
}
Test: TestTestify
=== TestGotestTools
assertion failed:
--- got
+++ want
main.User{
Name: "ada",
Roles: []string{
"admin",
- "dev",
+ "ops",
},
}
=== TestMatryerIs
cmp_test.go:32: dev != opsEach test compares a User with a Roles slice of two elements, and the second element differs: dev where ops was expected. It is a small difference in a nested value, the sort of mismatch that real tests have. Here is what each approach printed.
Plain standard library
t.Errorf("got %+v, want %+v", got, want) printed both values in full: got {Name:ada Roles:[admin dev]}, want {Name:ada Roles:[admin ops]}. For a small struct that is enough, because you can compare by eye. For a struct with thirty fields and a nested slice it is a wall of text where the one difference hides. The standard library also needs you to write the comparison, here a hand-written equal function, which is the more serious cost: it must be kept in step with the type.
Strength: no dependency, no new idiom. Weakness: you write the comparison and the diff yourself. Use it for scalar results and small values.
go-cmp
cmp.Diff(want, got) printed a structural diff:
main.User{
Name: "ada",
Roles: []string{
"admin",
- "ops",
+ "dev",
},
}Only the differing element is marked, with the surrounding structure for context. It compares deeply, handles unexported fields through options, ignores fields or sorts slices with cmpopts, and respects an Equal method on types. It is not an assertion library: you write if d := cmp.Diff(...); d != "" and decide how to fail, and the convention (-want +got) in the message tells the reader which side is which. Maintained by Google, it is the strongest answer to "what is different" in the ecosystem. The go-cmp article covers its options.
testify
assert.Equal(t, want, got) printed an Error Trace with the file and line, an Error block with Not equal:, the expected and actual values on single lines, and a unified diff of the pretty-printed values, with - (string) (len=3) "ops" and + (string) (len=3) "dev". It comes with require (stop on failure), suite for setup and teardown, mock and http helpers. It is by far the most used Go assertion library. The diff is useful, if more verbose than go-cmp's, and the expected, actual argument order trips people regularly. See the testify tutorial and the comparison with the standard library in stdlib versus testify.
gotest.tools
gtassert.DeepEqual(t, got, want) printed assertion failed: and a diff headed --- got and +++ want, built on go-cmp, so the diff has the same quality. The package has assert, assert/cmp for comparison helpers, fs for temporary file trees and golden for golden files. It is smaller and more opinionated than testify, with the diff as its default.
matryer/is
is.New(t).Equal(got.Roles[1], want.Roles[1]) printed one line, dev != ops, with the location. The API is tiny: is.Equal, is.NoErr, is.True, plus a one line failure that includes the source text of the assertion. It is pleasant for scalar values and has no diff for structures, which the lab shows by needing to compare the element and not the whole value. A good fit if you like less.
What the experiment says
| Approach | Compares deep values | Shows where they differ | Extra features |
|---|---|---|---|
stdlib t.Errorf | You write it | No | None |
| go-cmp | Yes | Yes, minimal | Options for ignoring, sorting, custom equality |
testify assert | Yes | Yes, as a diff | require, suites, mocks, HTTP helpers |
gotest.tools assert | Yes, through go-cmp | Yes | fs, golden, icmd |
matryer/is | Scalars | One line | Tiny API |
The summary is that the quality of the diff is shared by go-cmp and the libraries built on it, and the rest is style. Choose by the idiom your team prefers to read and the features you use.
A default
Use the standard library for the check itself and cmp.Diff for anything structured. This costs one dependency, go-cmp, which is maintained by Google, and gives the best failure message available. Add testify if the team wants the assertion vocabulary, require or suites; accept its extra output and its argument order. Whichever you pick, put the choice in the contributing guide, because a code base with three assertion styles is harder to read than any one of them.
Mocks
Assertion libraries check values. Mocks stand in for collaborators, and the choice is about generation.
Hand-written fakes are the first choice, for the reasons in the fakes, stubs and mocks article. A small interface and an eight-line struct beat a generated type for readability.
gomock (now maintained as go.uber.org/mock) generates a mock from an interface with mockgen and checks expected calls and arguments. It is strict by default: unexpected calls fail the test. The gomock and mockery article runs mockgen and shows the generated code.
mockery generates mocks that work with testify's mock package, with typed expecter methods. It is popular with teams that use testify. Version 3 needs a recent Go toolchain, so I describe it from its documentation and did not run it.
testify/mock lets you write mocks by hand against mock.Mock, with On("Method", args).Return(...). It is flexible and stringly typed, which means refactors are checked at run time.
The risk with all generated mocks is the same: tests that assert the order and number of calls are tests of the implementation, and they break when the implementation changes while behaviour does not. Mock at boundaries you do not own, a payment API, the clock, a message bus, and use fakes or real components elsewhere. See also the mock interface article.
Other libraries worth knowing
frankban/quicktest offers checkers with composable conditions. stretchr/testify/suite provides setup and teardown structure for tests that share a fixture, though t.Cleanup and TestMain cover most of that without a framework. go-snaps and autorest/golden-style helpers store expected output, as in snapshot testing. I did not run quicktest for this article.
Failure messages you can write yourself
A reason to prefer the standard library is that a good message is easy to write by hand and you control exactly what it says. A useful template is t.Errorf("Parse(%q) = %v, want %v", input, got, want): it names the function, shows the input, and puts the actual result before the expected one, matching how people read it. With go-cmp for structured values the template becomes t.Errorf("Parse(%q) mismatch (-want +got):", input) followed by the diff. In a table test this means the failure names the case without a debugger.
Libraries with a fixed message format give you less control over the input, which matters in loops. In a table of forty cases, assert.Equal(t, want, got) reports the mismatch but not which row it came from unless you pass a message argument, and many people forget. Pass the case name every time, or use subtests so the name is in the header. The same discipline applies to every library here.
What changes with parallel tests and subtests
All of these work with subtests and t.Parallel, because they take a *testing.T. Two details matter. A require-style assertion that calls t.FailNow must run on the test's own goroutine, not one it started, and go vet reports the mistake; assertion libraries cannot work around that rule. And a helper that calls t.Helper() makes the reported line the caller's, not the helper's, which every serious library does and a hand-written helper must remember to do.
Choosing, briefly
- Start with the standard library and
go-cmp. - If reading
t.Errorfblocks becomes tedious, adopt testify or gotest.tools as a team, once. - Generate mocks only for boundaries.
- Write down the choice, and stay with it.
The standard testing package is documented at pkg.go.dev/testing, go-cmp at pkg.go.dev/github.com/google/go-cmp/cmp, and testify at pkg.go.dev/github.com/stretchr/testify. The wider toolbox is in the testing tools roundup.


