{"id":213,"date":"2026-05-10T13:04:15","date_gmt":"2026-05-10T13:04:15","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/go-testify-2026-what-i-actually-use\/"},"modified":"2026-05-10T13:04:15","modified_gmt":"2026-05-10T13:04:15","slug":"go-testify-2026-what-i-actually-use","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/go-testify-2026-what-i-actually-use\/","title":{"rendered":"Go testify in 2026: What I Actually Use (and What I Dropped)"},"content":{"rendered":"<p>Confession: I spent four years writing Go tests with testify before I sat down and asked whether I actually needed it. The answer turned out to be &ldquo;for some things, yes; for most things, the standard library is fine, and I was just used to typing <code>assert.Equal<\/code>.&rdquo; This post is what I figured out, with code, after migrating one large service away from testify (mostly) and a smaller one onto it.<\/p>\n<p>If you&rsquo;re new to Go and the only test framework you&rsquo;ve heard of is <a href=\"https:\/\/github.com\/stretchr\/testify\" rel=\"nofollow noopener\" target=\"_blank\">testify<\/a>, this post will save you some reading. If you&rsquo;ve been using it for years, this post is the conversation I wish my older coworkers had with me when I started.<\/p>\n<p>Short answer: I still use <code>assert<\/code> and <code>require<\/code> for value comparisons in a few places. I don&rsquo;t use <code>mock<\/code> anymore, and I gave up on <code>suite<\/code>. The real win in 2026 is that the <a href=\"https:\/\/pkg.go.dev\/testing\" rel=\"nofollow noopener\" target=\"_blank\">standard <code>testing<\/code> package<\/a> plus a couple of small libraries cover 95% of what I used to reach for testify for.<\/p>\n<h2 id=\"what-testify-actually-gives-you\">What testify actually gives you<\/h2>\n<p>Testify is three packages bundled together: <code>assert<\/code>, <code>require<\/code>, <code>mock<\/code>, plus the <code>suite<\/code> runner. Each one solves a different real problem.<\/p>\n<p><code>assert<\/code> and <code>require<\/code> give you readable expectation helpers. Instead of writing <code>if got != want { t.Errorf(\"got %v, want %v\", got, want) }<\/code>, you write <code>assert.Equal(t, want, got)<\/code>. The output on failure includes both values and a diff for structs.<\/p>\n<p><code>mock<\/code> gives you a runtime mock object generator. You declare expected calls, run your code, and assert at the end that the mock was called as expected.<\/p>\n<p><code>suite<\/code> gives you a test class with <code>SetupTest<\/code>, <code>TearDownTest<\/code>, and shared state, like xUnit-style frameworks in other languages.<\/p>\n<p>These are real conveniences. They&rsquo;re also opinions about how testing should work, and the opinions don&rsquo;t always age well in Go specifically.<\/p>\n<h2 id=\"assert-vs-require-the-only-rule-i-still-follow\">assert vs require: the only rule I still follow<\/h2>\n<p>If you take one thing from this post: <code>assert<\/code> lets the test keep running after a failure, <code>require<\/code> stops it. That distinction matters more than people give it credit for.<\/p>\n<p>Here&rsquo;s a test where I want to check several independent things on a response:<\/p>\n<pre><code class=\"language-go\">func TestUserResponse(t *testing.T) {\n    resp := getUser(t, 42)\n\n    assert.Equal(t, 42, resp.ID)\n    assert.Equal(t, &quot;Qasim&quot;, resp.Name)\n    assert.NotEmpty(t, resp.Email)\n    assert.False(t, resp.Deleted)\n}\n<\/code><\/pre>\n<p>If <code>resp.Name<\/code> is wrong, I still want to know whether <code>resp.Email<\/code> is also wrong. Four <code>assert<\/code> calls, four independent checks, one test run.<\/p>\n<p>But here&rsquo;s a test where the second check is meaningless if the first one fails:<\/p>\n<pre><code class=\"language-go\">func TestParseConfig(t *testing.T) {\n    cfg, err := config.Parse(&quot;config.yaml&quot;)\n    require.NoError(t, err)        \/\/ if this fails, cfg is nil\n    require.NotNil(t, cfg)         \/\/ sanity check before dereferencing\n\n    assert.Equal(t, &quot;production&quot;, cfg.Env)\n    assert.Equal(t, 5432, cfg.DBPort)\n}\n<\/code><\/pre>\n<p>If <code>Parse<\/code> errored, <code>cfg<\/code> is nil, and dereferencing it in the next assertion would panic. <code>require.NoError<\/code> halts the test before that happens.<\/p>\n<p>The rule I live by: use <code>require<\/code> for preconditions (parsing succeeded, the response was non-nil, the database returned something) and <code>assert<\/code> for the actual things you&rsquo;re testing. If you find yourself writing <code>require.Equal<\/code> for the thing under test, you probably want <code>assert<\/code> so you see all failures, not just the first one.<\/p>\n<h2 id=\"where-i-stopped-using-mock\">Where I stopped using mock<\/h2>\n<p>I used <code>testify\/mock<\/code> for years. I don&rsquo;t anymore, and the reason is that Go&rsquo;s interface system makes it unnecessary.<\/p>\n<p>Here&rsquo;s what testify mocks look like:<\/p>\n<pre><code class=\"language-go\">type MockEmailer struct {\n    mock.Mock\n}\n\nfunc (m *MockEmailer) Send(to, subject, body string) error {\n    args := m.Called(to, subject, body)\n    return args.Error(0)\n}\n\nfunc TestSignup(t *testing.T) {\n    m := new(MockEmailer)\n    m.On(&quot;Send&quot;, &quot;user@example.com&quot;, &quot;Welcome&quot;, mock.Anything).Return(nil)\n\n    svc := signup.New(m)\n    err := svc.Register(&quot;user@example.com&quot;)\n\n    require.NoError(t, err)\n    m.AssertExpectations(t)\n}\n<\/code><\/pre>\n<p>That works. It&rsquo;s also a lot of ceremony for what&rsquo;s essentially &ldquo;did the function call Send with the right arguments.&rdquo; Here&rsquo;s the version I write now, with no testify:<\/p>\n<pre><code class=\"language-go\">type fakeEmailer struct {\n    sent []sentEmail\n}\n\ntype sentEmail struct{ to, subject, body string }\n\nfunc (f *fakeEmailer) Send(to, subject, body string) error {\n    f.sent = append(f.sent, sentEmail{to, subject, body})\n    return nil\n}\n\nfunc TestSignup(t *testing.T) {\n    f := &amp;fakeEmailer{}\n    svc := signup.New(f)\n\n    err := svc.Register(&quot;user@example.com&quot;)\n    if err != nil {\n        t.Fatal(err)\n    }\n\n    if len(f.sent) != 1 {\n        t.Fatalf(&quot;want 1 email, got %d&quot;, len(f.sent))\n    }\n    if f.sent[0].to != &quot;user@example.com&quot; {\n        t.Errorf(&quot;wrong recipient: %q&quot;, f.sent[0].to)\n    }\n}\n<\/code><\/pre>\n<p>More lines? Yes, by about six. But:<\/p>\n<p>I can see exactly what was sent and assert on it normally. There&rsquo;s no string-based method matching. Refactoring the <code>Emailer<\/code> interface (renaming <code>Send<\/code>, changing the signature) immediately fails to compile, instead of silently passing a test that mocked the old name.<\/p>\n<p>I don&rsquo;t have a <code>MockSomething<\/code> parallel hierarchy that I have to keep in sync with my real interface. The fake is just another implementation.<\/p>\n<p>The mock library does have one thing fakes don&rsquo;t: built-in argument matching like <code>mock.Anything<\/code>, <code>mock.AnythingOfType(\"string\")<\/code>. I&rsquo;ve found that when I need that, my test is doing too much. The fix is usually to make my interface narrower, not to match against <code>Anything<\/code>.<\/p>\n<h2 id=\"suite-i-gave-up-on-this-one\">suite: I gave up on this one<\/h2>\n<p><code>suite.Suite<\/code> lets you define <code>SetupTest<\/code> and <code>TearDownTest<\/code> methods on a struct, with shared state, like JUnit. I used it for database integration tests for a year. Then I switched to plain test helpers and was happier.<\/p>\n<p>The problem with <code>suite.Run<\/code> is that it hides what&rsquo;s happening. New people on the team had to learn the suite lifecycle (which methods get called when, in what order, what <code>SetupTest<\/code> vs <code>SetupSuite<\/code> does) instead of just reading the test top to bottom. I had bugs where state leaked between subtests because I&rsquo;d written <code>SetupSuite<\/code> instead of <code>SetupTest<\/code> and didn&rsquo;t notice.<\/p>\n<p>The plain Go version uses <code>t.Cleanup<\/code> and a helper:<\/p>\n<pre><code class=\"language-go\">func setupDB(t *testing.T) *sql.DB {\n    t.Helper()\n    db := openTestDB(t)\n    t.Cleanup(func() {\n        db.Exec(&quot;TRUNCATE users CASCADE&quot;)\n        db.Close()\n    })\n    return db\n}\n\nfunc TestCreateUser(t *testing.T) {\n    db := setupDB(t)\n    err := users.Create(db, &quot;qasim@example.com&quot;)\n    if err != nil {\n        t.Fatal(err)\n    }\n}\n<\/code><\/pre>\n<p>Each test sets up its own state. <code>t.Cleanup<\/code> runs in reverse order when the test ends, even if it failed or panicked. There&rsquo;s no shared receiver. If two tests need the same setup, I extract a helper. If they need different setup, they don&rsquo;t share one.<\/p>\n<p>This isn&rsquo;t an indictment of suite. If you&rsquo;ve used JUnit-style frameworks for years and the suite mental model is comfortable, fine. But the plain-Go version is right there, and it doesn&rsquo;t require remembering which lifecycle hook runs when.<\/p>\n<h2 id=\"what-i-actually-reach-for-in-2026\">What I actually reach for in 2026<\/h2>\n<p>For value comparisons that aren&rsquo;t deeply nested, plain <code>t.Errorf<\/code> with a clear message. The diff is rarely worth the dependency.<\/p>\n<p>For deeply nested struct comparisons, <a href=\"https:\/\/pkg.go.dev\/github.com\/google\/go-cmp\/cmp\" rel=\"nofollow noopener\" target=\"_blank\">google\/go-cmp<\/a>. It&rsquo;s by far the best diff output in Go and it has good options for ignoring unexported fields, comparing floats with tolerance, and so on:<\/p>\n<pre><code class=\"language-go\">if diff := cmp.Diff(want, got); diff != &quot;&quot; {\n    t.Errorf(&quot;response mismatch (-want +got):\\n%s&quot;, diff)\n}\n<\/code><\/pre>\n<p>For HTTP handlers, <code>httptest.NewRecorder<\/code> and reading <code>rec.Code<\/code>, <code>rec.Body.String()<\/code>. Faster than building a request mock, and exactly the API the standard library gives you.<\/p>\n<p>For database integration tests, plain helpers and <code>t.Cleanup<\/code>, plus <code>testing.Short()<\/code> to skip them on CI fast-runs.<\/p>\n<p>For cases where I&rsquo;d otherwise reach for <code>assert.Equal<\/code> heavily across a single repo, I still pull in testify. The four lines vs one line trade-off matters when you have hundreds of assertions. I just don&rsquo;t use <code>mock<\/code> or <code>suite<\/code> from it. Some thoughts on this kind of decision live in my <a href=\"https:\/\/abrarqasim.com\/blog\/go-error-handling-patterns-that-actually-help\" rel=\"noopener\">Go error handling patterns<\/a> post, since &ldquo;what to do when something fails&rdquo; applies to test code too.<\/p>\n<h2 id=\"migrating-off-testify-or-onto-it\">Migrating off testify (or onto it)<\/h2>\n<p>If you&rsquo;re considering moving off testify, don&rsquo;t do it all at once. The pattern that worked for me:<\/p>\n<p>Step one, stop adding new tests with <code>mock<\/code> and <code>suite<\/code>. Use fakes and helpers in any new code.<\/p>\n<p>Step two, when you touch an old test that uses mocks, replace it with a fake. Don&rsquo;t migrate just for the sake of migrating, but when you&rsquo;re already in there, take the ten minutes.<\/p>\n<p>Step three, leave <code>assert<\/code> and <code>require<\/code> in place. They&rsquo;re fine. Removing them for ideological reasons just makes diffs noisier without making the code better.<\/p>\n<p>If you&rsquo;re considering moving onto testify, the decision is smaller than it feels. Add it. Use <code>require<\/code> for preconditions and <code>assert<\/code> for checks. Skip <code>mock<\/code> and <code>suite<\/code> until you have a specific reason to use them. If you never have that reason, you&rsquo;ve spent zero time learning APIs you don&rsquo;t use.<\/p>\n<h2 id=\"what-to-try-this-week\">What to try this week<\/h2>\n<p>Pick one test file in your project that uses mocks heavily. Convert one mock to a fake (a struct that implements the same interface, with a slice that records calls). Run the test. See if you find the new version easier to understand. If yes, keep going. If no, the mock library is doing something for you, and you should figure out what.<\/p>\n<p>If you don&rsquo;t use testify yet, try this: write your next test with <code>require.NoError<\/code> for the precondition checks and <code>assert.Equal<\/code> for the actual checks. See if the failure messages are more useful than what you had before. If they are, you&rsquo;ve justified the dependency. If they aren&rsquo;t, the standard library was already doing fine.<\/p>\n<p>You can see a few of the Go services I&rsquo;ve shipped on <a href=\"https:\/\/abrarqasim.com\" rel=\"noopener\">my portfolio<\/a>, and they&rsquo;re a mix: some use testify, some don&rsquo;t. I stopped picking sides about this years ago. Picking the right testing tool for a specific test matters a lot more than picking the right framework for the whole repo.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Four years with testify, then I migrated off it. What I kept (assert and require), what I dropped (mock and suite), and the plain-Go alternatives I prefer.<\/p>\n","protected":false},"author":2,"featured_media":212,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"Four years with testify, then I migrated off it. What I kept (assert and require), what I dropped (mock and suite), and the plain-Go alternatives I prefer.","rank_math_focus_keyword":"go testify","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[147],"tags":[46,47,253,30,201],"class_list":["post-213","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-backend","tag-go","tag-golang","tag-testify","tag-testing","tag-tooling"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/213","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/comments?post=213"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/213\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/212"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=213"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=213"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=213"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}