Skip to content
gohacks.dev
Testing

Testify Assertions and Require in Go Tests Explained

Learn testify assert and require in Go: install, Equal, NoError, ErrorIs, Contains, ElementsMatch, JSONEq and Panics, with the real failure message of each.

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

The standard testing package gives you t.Errorf and t.Fatalf, and a Go test is a series of if got != want blocks. Testify packages those blocks as functions: assert.Equal(t, want, got) replaces four lines, and failure messages come with a diff. It is the most widely used third-party testing module in Go, and it deserves a plain explanation of what it does and where it is awkward.

Tested with Go 1.25.6, testify v1.11.1, macOS on Apple Silicon (arm64)

This tutorial covers assert and require on Go 1.25.6 with testify v1.11.1. Later articles cover suites, mocks and HTTP helpers. If you are weighing whether to use it at all, read the comparison of assertion libraries first.

Install and import

Shell
go get github.com/stretchr/[email protected]

Testify has several packages. assert and require hold the assertions, mock and suite the heavier tools, and http a small helper. In a test file you normally import two:

Go
import (
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

The two families

Every assertion exists in both packages with the same name and arguments. They differ in what happens on failure.

  • assert.Equal(t, want, got) records the failure with t.Errorf and returns false; the test goes on.
  • require.Equal(t, want, got) records it with t.FailNow and stops the test.

The distinction is the difference between t.Error and t.Fatal, and the rule for choosing is the same. Use require for steps that later lines depend on, such as require.NoError(t, err) before using the value the call returned. Use assert for independent checks, so one run reports every mismatch. The dedicated assert versus require article goes through the cases.

A first test

calc.goGo
package main

import (
	"encoding/json"
	"errors"
	"fmt"
)

var ErrDivZero = errors.New("division by zero")

func Div(a, b float64) (float64, error) {
	if b == 0 {
		return 0, fmt.Errorf("div %v/%v: %w", a, b, ErrDivZero)
	}
	return a / b, nil
}

type Item struct {
	SKU  string   `json:"sku"`
	Qty  int      `json:"qty"`
	Tags []string `json:"tags"`
}

func Encode(i Item) string { b, _ := json.Marshal(i); return string(b) }

func MustPositive(n int) int {
	if n <= 0 {
		panic("not positive")
	}
	return n
}

func main() {}
calc_test.goGo
package main

import (
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestPassingAssertions(t *testing.T) {
	got, err := Div(10, 4)
	require.NoError(t, err) // stop here if Div failed, the next line would be meaningless
	assert.Equal(t, 2.5, got)
	assert.InDelta(t, 0.3, 0.1+0.2, 1e-9)

	_, err = Div(1, 0)
	assert.ErrorIs(t, err, ErrDivZero)
	assert.EqualError(t, err, "div 1/0: division by zero")

	items := []string{"b", "a", "c"}
	assert.Len(t, items, 3)
	assert.Contains(t, items, "a")
	assert.ElementsMatch(t, []string{"a", "b", "c"}, items)

	assert.JSONEq(t, `{"qty":2,"sku":"x","tags":null}`, Encode(Item{SKU: "x", Qty: 2}))
	assert.Panics(t, func() { MustPositive(0) })
	assert.NotPanics(t, func() { MustPositive(1) })
	assert.Nil(t, nil)
	assert.Empty(t, []int{})
	t.Log("all passing assertions ran")
}

func TestFailingMessages(t *testing.T) {
	assert.Equal(t, 3, 4, "totals differ for order %d", 7)
	assert.Contains(t, []string{"a", "b"}, "z")
	assert.Len(t, []int{1, 2}, 3)
	assert.ErrorIs(t, nil, ErrDivZero)
}
Shell
go test -count=1 -v . 2>&1 | grep -vE '^(=== RUN|=== PAUSE|=== CONT|=== NAME|PASS|ok|FAIL$|FAIL\s|exit status)' | sed -E 's/ \([0-9.]+s\)//; s#^ +[a-z_0-9]+\.go:[0-9]+: #    #; s/\x1b\[[0-9;]*m//g; s#/[^[:space:]]*/([a-z_0-9]+\.go)#\1#g; s/[[:space:]]+$//' | grep -vE '^\s*$'
Text
    all passing assertions ran
--- PASS: TestPassingAssertions
        	Error Trace:	calc_test.go:34
        	Error:      	Not equal:
        	            	expected: 3
        	            	actual  : 4
        	Test:       	TestFailingMessages
        	Messages:   	totals differ for order 7
        	Error Trace:	calc_test.go:35
        	Error:      	[]string{"a", "b"} does not contain "z"
        	Test:       	TestFailingMessages
        	Error Trace:	calc_test.go:36
        	Error:      	"[1 2]" should have 3 item(s), but has 2
        	Test:       	TestFailingMessages
        	Error Trace:	calc_test.go:37
        	Error:      	Expected error with "division by zero" in chain but got nil.
        	Test:       	TestFailingMessages
--- FAIL: TestFailingMessages

TestPassingAssertions exercises the assertions you will use most. Going through them:

Equality. assert.Equal(t, 2.5, got) compares with ObjectsAreEqual, which uses reflect.DeepEqual and handles byte slices specially. Values must have the same type: Equal(t, 1, int64(1)) fails, which surprises people, and EqualValues converts first when you want that. assert.NotEqual is the inverse. For floating point use InDelta or InEpsilon, as the test does for 0.1 + 0.2, because the sum is not exactly 0.3.

Errors. require.NoError(t, err) guards the call before its result is used. assert.ErrorIs(t, err, ErrDivZero) checks the chain with errors.Is, which is the right comparison for wrapped errors; assert.ErrorAs is the errors.As version. assert.EqualError(t, err, "div 1/0: division by zero") compares the message text exactly, which couples the test to wording; prefer ErrorIs when a sentinel exists and ErrorContains for a fragment.

Collections. assert.Len checks length, assert.Contains works on strings, slices and map keys, and assert.ElementsMatch compares two slices ignoring order, which is the right tool for results built from a map or from goroutines. Empty, NotEmpty, Subset and Equal on slices complete the set.

Structured text. assert.JSONEq compares two JSON strings as data, so key order and whitespace do not matter: the test compares {"qty":2,"sku":"x","tags":null} with the output of Encode. YAMLEq does the same for YAML.

Panics. assert.Panics and assert.NotPanics take a function, run it and check whether it panicked. PanicsWithValue and PanicsWithError check what was panicked with.

Nil and zero. Nil and NotNil treat typed nil pointers and nil slices, maps and channels as nil; Zero and NotZero compare with the zero value of the type; True and False take a bool.

The test logs all passing assertions ran, and passes.

Reading failures

The second test fails on purpose, four times, to show what testify prints. The format is consistent:

Text
Error Trace:	calc_test.go:34
Error:      	Not equal:
            	expected: 3
            	actual  : 4
Test:       	TestFailingMessages
Messages:   	totals differ for order 7

The Error Trace is the file and line of the assertion, which t.Helper handling inside testify makes point at your test and not at the library. Error is the explanation; for Equal it shows both values and, for structures, a diff (see reading diffs). Test names the test or subtest, and Messages is the optional trailing arguments you passed: a format string and values, assert.Equal(t, 3, 4, "totals differ for order %d", 7).

Other assertions describe themselves:

  • Contains says []string{"a", "b"} does not contain "z".
  • Len says "[1 2]" should have 3 item(s), but has 2.
  • ErrorIs with a nil error says Expected error with "division by zero" in chain but got nil.

Because the output is long, a failing assertion in a loop is hard to place. Pass a message that names the case, or use subtests so the test name does. A message such as "case %q", c.name costs nothing and saves a debugging session.

The argument order trap

The signature is Equal(t, expected, actual). Go's standard library convention in the if got != want form is the opposite, and many people write assert.Equal(t, got, want) out of habit. The test still passes or fails correctly, but the failure message labels the values the wrong way round, and a reader chases the wrong side. Pick the order testify uses and keep to it: first what you expect, then what the code returned. A linter such as testifylint flags reversed arguments; it is described here and was not run.

Boolean checks and why to avoid them

assert.True(t, a == b) works and tells you nothing when it fails: Should be true. Prefer the specific assertion, assert.Equal(t, a, b), which prints both values. The same goes for assert.True(t, len(x) == 3) against assert.Len(t, x, 3), and for assert.True(t, err == nil) against assert.NoError(t, err). Reserve True and False for conditions that are naturally boolean, such as the result of a predicate function.

assert.New and require.New

Passing t to every call is repetitive in a long test. is := assert.New(t) returns an object whose methods drop the first argument: is.Equal(want, got). The suite package builds on the same idea. It is a style choice; the only trap is binding t once and then using it inside a subtest, where the subtest's own t is the right one. Create is inside t.Run.

Using it with the rest of the testing package

Testify does not replace testing. Subtests, t.Parallel, t.Cleanup and t.TempDir work as usual, and the assertions take whatever t you give them. Two rules apply because of how FailNow works. Call require assertions only from the test's own goroutine; from another goroutine they do not stop the test, and go vet flags the pattern. And in helpers that wrap assertions, call t.Helper() first so that the reported line is the caller's.

Equal, EqualValues, Same and friends

The family of equality assertions is where most confusion lives, so it is worth a short map.

AssertionComparesUse it for
EqualDeep equality; same type requiredMost values, structs, slices, maps
EqualValuesConverts to the same type firstint against int64, named types against their base
ExactlySame type and valueWhen the type is part of the check
Same / NotSamePointer identitySingletons, caches, "the same object"
ObjectsAreEqualThe function behind EqualWriting your own helper

assert.Equal(t, 1, int64(1)) fails with a message that shows both values as 1 and gives no hint that the types differ, which is a confusing moment. EqualValues is the fix when the difference is not meaningful. Do not reach for it by habit; a type mismatch is sometimes a real bug, such as a function returning int where a caller expects int64.

Equal on pointers compares what they point to, not the addresses, because ObjectsAreEqual uses reflect.DeepEqual. If you need identity, Same is the assertion. Two pointers to equal structs pass Equal and fail Same.

Errors in depth

Go errors are values, wrapped by fmt.Errorf("...: %w", err), and the assertions follow errors.Is and errors.As.

  • assert.ErrorIs(t, err, target) passes when any error in the chain equals the target. Use it with sentinel errors such as io.EOF, sql.ErrNoRows and your own.
  • assert.ErrorAs(t, err, &target) finds the first error in the chain that can be assigned to the target's type and sets it. Use it to read fields of a custom error type. The target must be a non-nil pointer to a type that implements error, and go vet checks this at compile time for the direct errors.As call but not for the testify one, so a wrong pointer level fails at run time.
  • assert.EqualError(t, err, "text") compares err.Error() exactly. It is fine for a leaf error whose text is a deliberate part of the interface, and brittle otherwise.
  • assert.ErrorContains(t, err, "fragment") is the middle ground.
  • assert.Error(t, err) and assert.NoError(t, err) check presence only. Prefer a specific assertion when the kind of error matters, since a test that only checks Error passes for any failure, including the wrong one. That is the most common weak assertion in Go tests: the code returns an error, but not the error you meant.

The error testing article covers the same ground with the standard library.

Time, floats and tolerance

assert.WithinDuration(t, expected, actual, delta) compares times with a tolerance, which suits values stamped by time.Now(). A better fix is to inject the clock so the value is exact, as in determinism. assert.InDelta and assert.InEpsilon compare floats by absolute and relative error. Use InDelta with a delta that reflects the precision you need, not a number that happens to pass.

Writing your own helper

When the same three assertions appear in many tests, wrap them. A helper function takes t *testing.T, calls t.Helper(), and uses require or assert inside. The reported line is then the caller's. Name it for the domain, requireUserValid(t, u), and keep it to one idea. A helper that returns a value after asserting, such as mustParse(t, s), lets the test read as a story. The risk is that helpers hide what is being checked; if a reader must open the helper to know, it is too clever.

Messages that help

The optional trailing arguments are a format string and values. Use them to name the case in loops, assert.Equal(t, c.want, got, "case %q", c.name), and to add context that the assertion cannot know, "after retry %d". Do not repeat what the assertion already prints. A message that says "values should be equal" adds nothing to Not equal:.

Costs

The module adds a dependency and a vocabulary. Reflection-based comparison is slower than == for hot loops, which matters in benchmarks, not tests. The error output is verbose, which helps in the first minute and annoys in the thousandth. Some teams prefer go-cmp for structural diffs and the standard library for the rest, as argued in the stdlib article. Testify's assertions are also stringly typed in their messages and loosely typed in their arguments: Equal takes any, so a type mismatch is a run-time failure, not a compile error.

Mistakes beginners make

  • Using assert where require is needed. assert.NoError(t, err) followed by got.Name panics with a nil dereference when the call failed, and the panic hides the original error. Use require for the guard.
  • Comparing floats with Equal. It passes until it does not. Use InDelta.
  • Comparing errors by message. Use ErrorIs; text changes, sentinels do not.
  • Asserting in goroutines. require in a goroutine does not stop the test. Collect results in the test goroutine.
  • Testing slices of structs with Contains. It uses ObjectsAreEqual for each element, so it works, but the failure output is long; compare with Equal on a projection when only one field matters.
  • Ignoring the return value. assert functions return a bool. if !assert.Equal(t, a, b) { return } lets a test stop early without require, when you want to keep the test going elsewhere.

Parallel tests and subtests

Assertions are plain function calls on *testing.T, so they work in subtests and parallel tests without any special handling. Inside t.Run, use the subtest's own t for every assertion, including those made through assert.New(t). A common bug is writing is := assert.New(t) before the loop and calling is.Equal inside t.Run: the failure is attached to the parent test, the subtest passes, and the report points at the wrong name. Create the bound assertion object inside the subtest function.

When tests are parallel, assertions are safe to call concurrently on different t values. The data they compare is your responsibility, and the race detector will tell you if two subtests share a variable. The patterns for tables are in the testify table tests article.

A starting set

If you adopt it, begin with six functions and add the rest as needed: require.NoError, assert.Equal, assert.ErrorIs, assert.Len, assert.Contains and assert.ElementsMatch. They cover most tests, and the failure messages are good enough to act on. The full list is in the testify documentation, and a compact table is in the cheat sheet.

Tagged""
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.