Testify is making your Go tests worse

April 20, 2026

How your assertion library hides bugs in plain sight, and what to do instead.

Pop quiz.

Does this testify assertion pass or fail?

	var x []int
	y := []int{}
	require.Equal(t, x, y)

If you’re like me, you have no idea. Arguments for both passing and failing seem reasonable. Let’s jump to the docs:

func Equal

func Equal(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool

Equal asserts that two objects are equal.

Well that’s no help. I still don’t know if an empty slice is considered equal to a nil slice! To answer the question, I have to dig through the source code, or just try it.

Is the learning curve worth it?

Assertion libraries are popular in Go, and for a solid reason: they make tests shorter. But in exchange, you’re signing up for some hefty costs, which are often overlooked. A few, from the official Go wiki:

  • They require developers to learn a whole new sub-language just to read or write tests
  • Most assertions abort a test early, omitting potentially useful information from the output
  • They hurt expressiveness, both in communicating what is being asserted, and in how you present failures
  • They make it easy to write imprecise tests
  • They duplicate features already found in the language, such as evaluation, comparison, and often more

And a few more reasons of my own:

  • Many assertion libraries are internally inconsistent (testify is one such example)
  • They require a combinatoric explosion of functions to express slight variants on certain checks, which are much easier expressed using first-class language features such as ! or &&

If any of these downsides are true, why do assertion libraries remain popular in Go?

In a word: Habit. In many languages, there is no testing framework provided by the language. This means that to do unit testing, you must use a third-party tool, and historical momentum has made xUnit-style testing libraries common. So when folks start learning Go, they naturally carry those habits with them, because it’s comfortable.

But Go treats testing as a first-class citizen, shipping with its own testing framework, as well as the testing package, and related libraries, included in the standard library. So the explicit need for a third-party testing framework isn’t there. Is there sufficient reason to still prefer a third-party assertion library? Below are the most common arguments in favor, and why they don’t really hold up.

  1. “It’s easier to read.” — While “readability” is mostly subjective, what’s readable is mostly what’s familiar. If all else is equal, any convention can be readable if you’re familiar with it. As we’ll discuss later, though, assertion libraries, in general are not equal.
  2. “It’s shorter.” — While technically true, this is mostly irrelevant. Yes, it makes many assertions shorter, but is that a virtue? Not if it makes them harder to understand.
  3. “It makes complex comparisons easier.” — If you’re comparing assert.Equal to a long list of individual struct field comparisons, yes, the assert library is the clear winner on all counts: easier to write, easier to read, easier to reason about. But there’s an alternative that’s better than either of these.

The Go philosophy

Why does Go do tests differently from so many other languages?

It’s not a dig at other languages. It’s a matter of Go’s philosophy. Every language prioritizes certain things over others. And one thing Go prioritizes is making things obvious. Go’s notoriously verbose error handling is a good example of this principle in action. You’ve no doubt written if err != nil more times in the last year than you’ve written your own postal code in your entire lifetime. And while this is an endless source of annoyance for some, it exists for a reason: It makes it obvious when an error might occur, and how it’s being handled. The common alternative of exceptions, by comparison, makes problems opaque.

Assertion libraries are to Go’s testing philosophy what exceptions are to Go’s error handling.

An idiomatic Go test

Let’s pause to look at what an idiomatic Go test would actually look like, as a point of reference for the rest of this discussion. Here’s what the test from the quiz above might look like, written using only the testing library:

	var x []int
	y := []int{}
	if !slices.Equal(x, y) {
		t.Error("expected x and y to be equal")
	}

And, of course, you’ll be forgiven if you think this looks less “readable” than the assert version above. It certainly is more verbose. But it gives us several improvements over the testify-based version. Let’s talk about them.

“Readability” FTW

It’s time for the big reveal: the assert.Equal(x, y) call from the quiz will fail. Testify treats empty and nil slices as unequal.

In contrast, slices.Equal(x, y) returns true, so that version of the test will pass. That’s different! Why?

Well, while assert.Equal’s precise behavior isn’t explicitly documented, slices.Equal’s is (emphasis added):

func Equal

func Equal[S ~[]E, E comparable](s1, s2 S) bool

Equal reports whether two slices are equal: the same length and all elements equal. If the lengths are different, Equal returns false. Otherwise, the elements are compared in increasing index order, and the comparison stops at the first unequal pair. Empty and nil slices are considered equal. Floating point NaNs are not considered equal.

Now as I said at the top of the post, I can imagine arguments both for and against nil and empty slices being considered equal. And I don’t know exactly why the Go team decided that slices.Equal should treat them as equal—though I suppose that in the context of slice comparison, for practical purposes, they are generally equal. But I do know that I use the slices package all the time in my code, and knowing how slices.Equal works is valuable information regardless of how I write my tests. If I use assert.Equal in my tests, I have to learn a second set of rules just for my tests.

Comparing assert.Equal to slices.Equal might seem like a nitpick. And if that were the only issue with using an assertion library, it probably would be. Here are a few other quiz questions for you:

  • Does calling Contains on a map check against keys, values, or both? (answer: keys)
  • What’s the difference between Equal and EqualValues? (answer: EqualValues coerces values for comparison)
  • How does Empty treat bool values? (answer: zero values are considered empty, so false is “empty”)

testify is a whole new language

Of course, testify doesn’t expose just one equality assertion function. Let’s take a quick look at the other equality functions included in the package:

  • Equal
  • EqualError
  • EqualExportedValues
  • EqualValues

And most of these have Not* and *f variants, for a total of 12 equality assertion functions.

Oh, but I forgot these:

  • Exactly
  • False
  • Nil
  • Same
  • True
  • Zero

These are also equality assertions by a different name. That makes a total of 30 functions that all handle subtly different senses of exact equality—effectively wrapping either == or !=. And that’s to say nothing of the over 100 other assert functions in the library, many of which directly mirror functionality built into the standard library, with slightly different calling conventions; Contains, DirExists, and LessOrEqual to name a few.

That’s a lot of assertion functions to either memorize, or be constantly looking up, every time you write or read a test. Is it really easier to read something that requires looking up words in the dictionary all the time?

Okay, but what about complex data structures?

Nobody wants to read or write this test:

	if person.Name != "Alice" {
		t.Errorf("Expected Name = Alice, got %s", person.Name)
	}
	if person.Age != 30 {
		t.Errorf("Expected Age = 30, got %d", person.Age)
	}
	if person.Email != "alice@example.com" {
		t.Errorf("Expected Email = alice@example.com, got %s", person.Email)
	}
	if !person.CreatedAt.Equal(time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)) {
		t.Errorf("Expected CreatedAt = 2020-01-01, got %s", person.CreatedAt)
	}

This is one of the most common objections I hear when presenting about avoiding assertion libraries. “So the Go wiki says to avoid assert libraries, but I don’t want to write 50 lines of boilerplate code to test my structs!” Fair criticism! But the Go wiki also says something else:

If your function returns a struct, don’t write test code that performs an individual comparison for each field of the struct. Instead, construct the struct that you’re expecting your function to return, and compare in one shot using diffs or deep comparisons. The same rule applies to arrays and maps.

That sounds like a valuable thing that assertion libraries actually provide! But can we get that value without the weight of an assertion library? Sure! cmp.Diff is the tool that the Go wiki suggests for just this problem. And not only does it replace the one-shot, deep-equality checks of most assertion libraries, it does it with a lot more flexibility!

cmp.Diff is part of the github.com/google/go-cmp/cmp package, from Google. It’s not part of the standard library (at least not yet, though it has been considered for inclusion), but it is from Google, and is officially recommended by the Go team.

So how does it work? Let’s illustrate by revisiting our original quiz example one more time:

	var x []int
	y := []int{}
	if d := cmp.Diff(x, y); d != "" {
		t.Errorf("expected x and y to be equal, diff: %s", d)
	}

In this version of the test, cmp.Diff returns a textual diff representation of the difference between the two arguments. In this case, it returns:

  []int(
- 	nil,
+ 	{},
  )

The - line is the first argument (x, our nil slice), and the + line is the second (y, the empty slice). One line, one type label, one difference called out. So now we’re back to testify’s semantics—treating nil and empty as distinct. But remember when I said cmp.Diff is flexible? We can instruct it to treat empty and nil slices as equal, if that’s the correct behavior for our application. Just one line changes:

	if d := cmp.Diff(x, y, cmpopts.EquateEmpty()); d != "" {

And beyond that, the cmpopts package provides the ability to ignore specific fields, do approximate time-matching, and a ton of other useful options. It even lets you write your own custom comparison functions, when you really need to get into the details.

The exception to the rule

Having made the case against using testify, I have a confession to make: I was writing tests with testify just this morning.

That’s because there’s one specific case where I see using testify, or other assertion libraries, as justifiable: You’re writing code for a team whose established convention dictates it.

One of my clients uses testify for their tests. Now, I could use the idiomatic Go approach when adding tests to their codebase. And maybe the individual tests would be better as a result. But the project as a whole would suffer. Rather than a single, coherent testing style, there would be two conflicting styles. If my goal is to reduce the burden of reading tests, consistently using testify is clearly better than using an alternative half of the time. That’s what I was doing this morning.

Unless the team has decided to migrate from one test style to another, I’d rather adhere to established convention.

Going deeper

Writing idiomatic tests in Go is a lot more than just avoiding testify. How do you avoid the overuse of mocks? How do you test code that calls a live, third-party service? How can you avoid hard-coded sleeps when testing concurrency? Is there any way to avoid flaky tests that depend on the clock? If these are topics you’d like me to help you explore, I’d like to invite you to join my live 6-week course, Idiomatic Testing in Go, starting May 5. It’s a small group: 15 seats, weekly 90-minute sessions, and I review your actual test code each week. Early bird enrollment ends April 28.

See the syllabus and reserve your seat.


Share this

Direct to your inbox, daily. I respect your privacy .

Unsure? Browse the archive .

Get daily content like this in your inbox!

Subscribe