Testify or the Standard Library for Go Test Assertions
Compare testify assert and require with plain if and t.Errorf in Go tests, using the real failure messages each produces and a recommendation by project.

Every Go team eventually has the argument. One side writes if got != want { t.Errorf(...) } and calls assertion libraries unnecessary. The other side writes assert.Equal(t, want, got) and counts the saved lines. Both are right about something, and the disagreement is rarely settled by evidence. This article puts the same four checks through each style, shows the exact failure output, and ends with a recommendation that depends on what you are building. It sits beside comparing values with cmp.Diff and the style guide in table-driven tests. It ran on Go 1.25.6 with testify v1.11.1.
Tested with Go 1.25.6, testify v1.11.1, macOS on Apple Silicon (arm64)
Four checks, three styles
The lab has a find function and four tests, each failing on purpose so the output shows what the reader gets.
package main
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type user struct {
ID int
Name string
Roles []string
}
var errNotFound = errors.New("not found")
func find(id int) (user, error) {
if id == 1 {
return user{ID: 1, Name: "Ada", Roles: []string{"admin", "dev"}}, nil
}
return user{}, errNotFound
}
// 1. The standard library: no dependency, a little more typing, a message you write yourself.
func TestStdlibStyle(t *testing.T) {
got, err := find(2)
if err == nil {
t.Fatalf("find(2) = %v, nil; want an error", got)
}
if !errors.Is(err, errNotFound) {
t.Errorf("find(2) error = %v, want %v", err, errNotFound)
}
got, err = find(1)
if err != nil {
t.Fatalf("find(1) unexpected error: %v", err)
}
if got.Name != "Eve" { // deliberately wrong
t.Errorf("find(1).Name = %q, want %q", got.Name, "Eve")
}
}
// 2. testify assert: one line per check, a built-in message, and the test keeps going.
func TestAssertStyle(t *testing.T) {
got, err := find(1)
assert.NoError(t, err)
assert.Equal(t, "Eve", got.Name) // deliberately wrong
assert.Equal(t, []string{"admin", "ops"}, got.Roles)
}
// 3. testify require: the same checks, but a failure stops the test, like t.Fatal.
func TestRequireStyle(t *testing.T) {
_, err := find(2)
require.NoError(t, err) // fails here; the next line never runs
t.Log("not reached")
}
// 4. The classic trap: assert.Equal compares types too.
func TestTypeTrap(t *testing.T) {
var count int64 = 3
assert.Equal(t, 3, count)
assert.EqualValues(t, 3, count)
}package main
func main() {}strip() { sed -E 's/ \([0-9.]+s\)//'; }
for name in TestStdlibStyle TestAssertStyle TestRequireStyle TestTypeTrap; do
echo "=== $name"
go test -count=1 -run "^$name\$" . 2>&1 | grep -vE '^(FAIL|ok)|^\s*$|Error Trace|^\s+Test:|^---' | sed -E 's/^[[:space:]]{2,}/ /' | strip
done=== TestStdlibStyle
assert_test.go:40: find(1).Name = "Ada", want "Eve"
=== TestAssertStyle
assert_test.go:48:
Error: Not equal:
expected: "Eve"
actual : "Ada"
Diff:
--- Expected
+++ Actual
@@ -1 +1 @@
-Eve
+Ada
assert_test.go:49:
Error: Not equal:
expected: []string{"admin", "ops"}
actual : []string{"admin", "dev"}
Diff:
--- Expected
+++ Actual
@@ -2,3 +2,3 @@
(string) (len=5) "admin",
- (string) (len=3) "ops"
+ (string) (len=3) "dev"
}
=== TestRequireStyle
assert_test.go:55:
Error: Received unexpected error:
not found
=== TestTypeTrap
assert_test.go:62:
Error: Not equal:
expected: int(3)
actual : int64(3)The standard library
TestStdlibStyle uses if and t.Errorf. The message is whatever you write, and the output is a single line:
assert_test.go:40: find(1).Name = "Ada", want "Eve"It is as good as the message: it names the call, the field, the value and the expectation. The cost is typing, and the risk that a hurried message omits something (the input, or the want). The code has no dependency and no new vocabulary: anyone who can read Go can read the test. t.Fatalf and t.Errorf give you the control over stopping that the other styles imitate.
testify assert
TestAssertStyle states the same things in one line each: assert.NoError(t, err), assert.Equal(t, "Eve", got.Name). On failure it prints a block with expected, actual and a diff:
Error: Not equal:
expected: "Eve"
actual : "Ada"
Diff:
--- Expected
+++ Actual
@@ -1 +1 @@
-Eve
+Adaand the second check, on a slice, shows a diff of the two values with only the changed element marked. The test also kept going after the first failure and reported both mismatches in one run, because assert marks the test failed and continues, like t.Errorf. The messages are consistent, and you get them without writing them. For bigger values the diff is genuinely helpful; for a single string it is more text than needed.
testify require
TestRequireStyle uses require.NoError(t, err) where find(2) returns an error. The output is the error, not found, and the line not reached was never logged: require calls t.FailNow and ends the test. Use require for preconditions where continuing is pointless, as you would use t.Fatal, and assert for the checks whose failures are independent.
The trap in the fourth test
assert.Equal(t, 3, count) where count is an int64:
expected: int(3)
actual : int64(3)It fails, with a message that shows the types. assert.Equal uses ObjectsAreEqual, which compares with reflect.DeepEqual semantics (and bytes.Equal for byte slices), so int(3) is not int64(3). This surprises everyone once. assert.EqualValues converts first and passes. The same strictness is correct for a type-safe test, and a nuisance with untyped constants. The standard library version, got != 3, compiles and works, because the constant takes the type of got.
Other things worth knowing about testify:
- Argument order is
(t, expected, actual), the opposite of the Go convention for tests in some codebases (got, want), and getting it backwards produces a failure message that lies about which value is which. - Message arguments come last and are optional (
assert.Equal(t, want, got, "for user %d", id)). - Helpers exist for many shapes:
ErrorIs,ErrorAs,Contains,ElementsMatch(equal ignoring order),JSONEq,Eventually,Panics,WithinDuration. They save real code for those cases. suitegives xUnit-style setup and teardown methods, which many Go programmers dislike as unidiomatic next tot.Cleanupand subtests.mockgenerates and verifies mock objects, with strengths and the usual hazards of mocking (mocks).- Dependency weight: adding testify to a module added three indirect modules:
go-spew(pretty printing),go-difflib(diffs) andyaml.v3. Small, widely used and stable, but they are in your supply chain and yourgo.sum.
What each style is good at
| Concern | Standard library | testify |
|---|---|---|
| Dependencies | none | testify plus three modules |
| Lines per check | 3 to 4 | 1 |
| Failure message | what you wrote | consistent, includes a diff |
| Collections and structs | needs cmp.Diff or reflect.DeepEqual | built in, ElementsMatch for unordered |
| Control over stop/continue | Error versus Fatal | assert versus require |
| Surprises | forgetting a message detail | type-sensitive Equal, argument order |
| Readability for newcomers | needs only Go | needs to learn the API |
| Matches the Go project's own style | yes | no |
Moving between styles
The choice is not permanent. Going from testify to the standard library is mechanical: assert.Equal(t, want, got) becomes if got != want { t.Errorf("... = %v, want %v", got, want) } for comparable values and cmp.Diff for structured ones; require becomes t.Fatalf. Going the other way is equally easy. If you are unsure, start with the standard library, add cmp when you first need a struct diff, and add testify only if the team asks for it. Removing a dependency that is everywhere in the tests is a bigger job than adding one later.
One practical test of whether your assertions are good: break the code on purpose and read the failure. If you can name the bug from the message alone, without opening the test, the assertion is doing its job. If you have to add a print statement and rerun, improve the message, whatever the library.
When the tests are the documentation
A test is often the first place a new developer looks to learn what a function does. Plain Go with clear names (got, want, a table of cases) reads like a specification to anyone. Assertion DSLs read well to people who know them. Weigh that against your team's turnover and who contributes: an open-source library that wants outside contributors benefits from tests that need no knowledge of a third-party package.
A recommendation
Libraries and small projects: the standard library with cmp.Diff for anything structured. Your tests then add one small dependency (go-cmp), have the diff quality of testify for structs and slices, and read as plain Go. This is also what the Go team does in its own repositories, and what the wiki's test comments recommend: use got and want, print the inputs, and avoid assertion libraries that hide the message.
Large application codebases where many people write tests daily: testify is a reasonable choice, provided the team picks it deliberately and uses it consistently. The saved lines add up, the messages are uniform, and the require/assert split encodes a useful habit. The worst outcome is a mix: half the tests one way, half the other, so a reader needs both vocabularies.
Either way:
- Never hide the inputs. A message that says only "not equal" fails the 3 a.m. test.
- Use table tests and subtests so one failing row names itself (table-driven tests).
- Put
t.Helper()in any assertion helper you write (t.Helper). - Do not mix both in one file.
The testify documentation is at pkg.go.dev/github.com/stretchr/testify.


