Remove Testify From a Project and Use the Standard Library
Remove testify from a Go project in steps: map assert and require calls to if statements, keep go-cmp for diffs, convert mocks to fakes and drop the module.

Testify is a fine library and not a necessary one. Teams remove it for three reasons: they want fewer dependencies in the code everyone reads, they prefer failure messages they write themselves, or they have found that the assertion vocabulary is heavier than the checks need. If you are in that position, the migration is mechanical and can be done a package at a time. This article gives the mapping and the order of work, with a converted test that runs on Go 1.25.6.
Tested with Go 1.25.6, testify v1.11.1, macOS on Apple Silicon (arm64)
The migration of suites has its own article, moving off testify suites. This one covers assertions and mocks.
One test, converted
package main
import (
"errors"
"testing"
)
var ErrNotFound = errors.New("not found")
func Find(id int) (string, error) {
if id == 1 {
return "ada", nil
}
return "", ErrNotFound
}
// The same checks as a testify test would make, written with the standard library.
func TestFindStdlib(t *testing.T) {
got, err := Find(1) // require.NoError
if err != nil {
t.Fatalf("Find(1) error: %v", err)
}
if got != "ada" { // assert.Equal
t.Errorf("Find(1) = %q, want %q", got, "ada")
}
_, err = Find(2)
if !errors.Is(err, ErrNotFound) { // assert.ErrorIs
t.Errorf("Find(2) error = %v, want ErrNotFound", err)
}
list := []string{"a", "b"} // assert.Len, assert.Contains
if len(list) != 2 {
t.Errorf("len = %d, want 2", len(list))
}
}
func main() {}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*$'--- PASS: TestFindStdlibThe test above is the standard library form of this testify test:
got, err := Find(1)
require.NoError(t, err)
assert.Equal(t, "ada", got)
_, err = Find(2)
assert.ErrorIs(t, err, ErrNotFound)
list := []string{"a", "b"}
assert.Len(t, list, 2)
assert.Contains(t, list, "a")Each testify call is a short if. The comments in the lab file mark the correspondence. The run passes, so the checks are equivalent for the cases written.
The mapping
| Testify | Standard library |
|---|---|
require.NoError(t, err) | if err != nil { t.Fatalf("F(...) error: %v", err) } |
assert.NoError(t, err) | if err != nil { t.Errorf(...) } |
assert.Error(t, err) | if err == nil { t.Error("want an error") } |
assert.ErrorIs(t, err, X) | if !errors.Is(err, X) { t.Errorf(...) } |
assert.ErrorAs(t, err, &T) | if !errors.As(err, &target) { t.Errorf(...) } |
assert.Equal(t, want, got) | if got != want { t.Errorf("F() = %v, want %v", got, want) } for comparable types |
assert.Equal on slices | slices.Equal(got, want) |
assert.Equal on maps | maps.Equal(got, want) |
assert.Equal on structs with slices or maps | reflect.DeepEqual, or cmp.Diff from go-cmp |
assert.ElementsMatch(t, a, b) | Sort copies with slices.Sort, then slices.Equal |
assert.Len(t, s, n) | if len(s) != n { t.Errorf(...) } |
assert.Contains(t, s, x) | slices.Contains(s, x) or strings.Contains |
assert.Empty(t, s) | if len(s) != 0 { ... } |
assert.Nil(t, p) | if p != nil { ... } |
assert.True(t, c) | if !c { t.Error(...) } |
assert.InDelta(t, a, b, d) | if math.Abs(a-b) > d { ... } |
assert.JSONEq(t, a, b) | Unmarshal both into any, then reflect.DeepEqual |
assert.Panics(t, f) | A helper using defer and recover |
assert.Eventually | A polling loop, or synctest |
assert.WithinDuration | if d := a.Sub(b).Abs(); d > delta { ... } |
mock.Mock | A hand-written fake |
suite.Suite | TestMain, helpers and t.Cleanup |
What you give up
Failure messages. You write them, and that is mostly a gain: "Find(%d) = %q, want %q" names the function and the input, which testify's generic message does not. The loss is the automatic diff. For structures, add go-cmp: a short if on cmp.Diff that calls t.Errorf with the diff. The diff is better than testify's. That adds a dependency, a smaller and more focused one.
Brevity. assert.Equal(t, want, got) is shorter than the if. For a test with many checks on one result, a helper like func eq[T comparable](t *testing.T, got, want T) with t.Helper() restores the one-line form for scalar values without a library.
Convenience matchers. Contains, Subset, ElementsMatch, InDelta and JSONEq have no one-line equivalents. The standard library has slices.Contains, strings.Contains and slices.Sort, which cover most of them in two lines.
Mocks. Generated or testify mocks become fakes: small structs with the methods the test needs, recording what they were called with. See fakes, stubs and mocks.
What you gain
go vetchecks your format strings. A mistake int.Errorf("%d", "x")is reported at vet time. Testify's message arguments are checked by nothing.- Compile-time types.
got != wantrequires matching types;assert.Equal(t, 1, int64(1))compiles and fails. - One fewer module in
go.modand its transitive dependencies ingo.sum, and a smaller surface to audit. - No
FailNowin the wrong goroutine. The calls are visiblyt.Fatal, andgo vetflags the goroutine misuse. - Messages that name the case. Because you write them, they say what you want to know at 3 a.m.
An order of work
- Count.
grep -rl "stretchr/testify" --include=*.go . | wc -lshows the size of the job. - Convert mocks and suites first if you have them, since they have the most structure.
- Convert assertions package by package. Start with the simplest tests, where
NoError,EqualandLendominate. - Introduce small helpers for the repeated shapes, one generic
equaland onenoError, in an internal test package; see shared helpers. - Add go-cmp if structured comparisons need diffs.
- Run the suite with
-race -shuffle=onafter each package; a conversion can change behaviour where a testify call tolerated an unexpected type. - Delete the dependency with
go mod tidyonce the last import is gone, and confirm withgo list -m all | grep testify.
A semi-automatic approach is to run a regular expression replacement for the simplest forms and review the diff by eye. For anything more, edit by hand: the point of the migration is the messages, and they need a person.
Writing the small helpers
Two helpers cover most of what people miss after the removal. Put them in an internal test package, as in shared test helpers, with t.Helper() as the first statement of each.
equal[T comparable](t, got, want)fails withgot X, want Yand works for numbers, strings, booleans and comparable structs. It restores the one-line form for scalar checks.noError(t, err)callst.Fatalfwith the error text. It is a one-line guard likerequire.NoError.
Resist adding a helper per assertion you lost. Each helper hides a comparison, and a library of thirty of them is a smaller testify with fewer features and no documentation. Add one when a check appears in twenty tests, not when it appears in two.
For checks on collections, the slices and maps packages (Go 1.21 and later) give you Equal, Contains, Sort, Index and Compact, enough for most conversions. For deep structures, reflect.DeepEqual answers "equal or not" and cmp.Diff explains the difference. If the project already depends on go-cmp for other reasons, that decides it.
Verifying the conversion
A mechanical rewrite can change what a test checks without anyone noticing, so verify. Before the conversion, record the list of passing test names with go test -v ./... | grep -- '--- PASS' | sort > before.txt; after it, produce after.txt the same way and diff. Then break the code on purpose in two or three places and confirm the converted tests fail, ideally with a mutation tool as in the mutation testing article. A converted assertion that is wrong in a way that always passes, for instance a missing ! in an if, is the typical error of a hand conversion, and the deliberate break finds it.
When to stop short
A mixed state is acceptable. Many projects keep testify for assertions on JSON and collections, where it is convenient, and remove suites and mocks. If the team likes the vocabulary and the failure output, the cost of converting thousands of calls may not repay. The case for removal is strongest in libraries, whose test dependencies show up in users' module graphs, and in small, security-sensitive services where every dependency is reviewed. The comparison of approaches is in the libraries roundup and in the stdlib article. The standard library pieces are documented in the testing, errors and slices packages.


