Skip to content
gohacks.dev
Testing

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.

7 min read
Summarize in AI
Flat illustration of a test report with one failing test.

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.

assert_test.goGo
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)
}
main.goGo
package main

func main() {}
Shell
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
Text
=== 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:

Text
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:

Text
Error:      	Not equal:
expected: "Eve"
actual  : "Ada"
Diff:
--- Expected
+++ Actual
@@ -1 +1 @@
-Eve
+Ada

and 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:

Text
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.
  • suite gives xUnit-style setup and teardown methods, which many Go programmers dislike as unidiomatic next to t.Cleanup and subtests.
  • mock generates 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) and yaml.v3. Small, widely used and stable, but they are in your supply chain and your go.sum.

What each style is good at

ConcernStandard librarytestify
Dependenciesnonetestify plus three modules
Lines per check3 to 41
Failure messagewhat you wroteconsistent, includes a diff
Collections and structsneeds cmp.Diff or reflect.DeepEqualbuilt in, ElementsMatch for unordered
Control over stop/continueError versus Fatalassert versus require
Surprisesforgetting a message detailtype-sensitive Equal, argument order
Readability for newcomersneeds only Goneeds to learn the API
Matches the Go project's own styleyesno

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.

Also inTooling
Taggedstdlib
Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.