The shopping cart test
You write a function that calculates the total price of a shopping cart. You write a test. The result comes back. You need to check if the result matches the expected value. The standard library gives you t.Errorf and t.Fatal. You end up writing if got != want { t.Fatalf("got %v, want %v", got, want) } every single time. It works, but the noise drowns out the logic. You want a way to say "this should equal that" without the boilerplate. That's where testify comes in.
Filling the gap in the standard library
The testing package in Go is intentionally small. It provides the runner and a way to report failures. It does not provide assertion helpers. Go's philosophy favors explicit control flow. The language gives you the tools to build your own assertions if you want. testify is a community solution that solved the problem so you don't have to. It's not part of the language, but it's part of the ecosystem. Almost every open-source Go project uses it.
testify adds assertion functions. assert checks a condition. If it fails, it logs an error but keeps running. require checks a condition. If it fails, it stops the test immediately. Think of assert as a warning light on your dashboard. The car keeps driving. require is the engine cutting out. The car stops immediately so you don't drive into a wall.
Minimal example
Here's the simplest test using testify. You import the assert package. You write a test function. You call assert.Equal with the test instance, the expected value, and the actual value.
package main
import (
"testing"
"github.com/stretchr/testify/assert"
)
// TestAdd verifies basic arithmetic using testify assertions
func TestAdd(t *testing.T) {
result := 2 + 2
// assert.Equal compares expected and actual. It logs a failure if they differ but continues execution
assert.Equal(t, 4, result)
}
How it works under the hood
When you run go test, the testing package finds TestAdd. It creates a *testing.T instance and passes it to your function. Inside, assert.Equal receives t. It compares 4 and result. If they match, the function returns silently. If they don't match, assert.Equal formats a message showing the difference and calls t.Errorf. The test marks itself as failed but keeps running any code that follows. This matters when you have multiple checks in one test. You get to see all the failures at once instead of stopping at the first one.
The t parameter always goes first in assertion calls. This matches the standard library pattern where t is the receiver of the test actions. The convention is to import github.com/stretchr/testify/assert and github.com/stretchr/testify/require. You rarely import the whole testify package. The community accepts the dependency because it saves time and reduces errors in test code.
Output formatting saves time
When assert.Equal fails, it doesn't just say "not equal". It prints the expected value and the actual value. For strings, it shows the difference. For structs, it uses reflection to show which fields differ. This saves you from writing custom comparison logic. You get a clear picture of what went wrong without digging through logs.
Sometimes you check a boolean. assert.True(t, condition) is clearer than assert.Equal(t, true, condition). The library provides helpers for common checks like NotEmpty, Contains, Panics, and Eventually. These functions make the intent of the test obvious. You read the test and understand what it verifies without parsing complex if statements.
Realistic example with multiple checks
Real tests often check multiple properties of a result. You fetch a user from a database. You want to verify the name, the age, and that the email is valid. Using assert lets you check everything. If the name is wrong and the age is wrong, you see both errors in the output. This saves time when debugging.
package main
import (
"testing"
"github.com/stretchr/testify/assert"
)
// User represents a basic user model
type User struct {
Name string
Age int
}
// GetUser returns a sample user for testing
func GetUser() User {
return User{Name: "Alice", Age: 30}
}
// TestUserValidation checks multiple fields of a user struct
func TestUserValidation(t *testing.T) {
user := GetUser()
// assert.NotEmpty checks that the string is not empty
assert.NotEmpty(t, user.Name)
// assert.Greater checks that age is positive
assert.Greater(t, user.Age, 0)
// assert.Equal verifies the exact name match
assert.Equal(t, "Alice", user.Name)
}
Handling async code with Eventually
Testing async code is hard. You start a background goroutine. You need to wait for it to finish. assert.Eventually helps here. You pass a function that checks the condition. testify runs it repeatedly until it returns true or the timeout hits. This avoids flaky tests where you use time.Sleep to wait for things. time.Sleep is brittle. It might wait too long or not long enough. Eventually adapts to the actual timing.
package main
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
// TestEventually checks a condition that becomes true over time
func TestEventually(t *testing.T) {
counter := 0
// Start a goroutine that updates the counter after a delay
go func() {
time.Sleep(100 * time.Millisecond)
counter = 1
}()
// assert.Eventually polls the function until it returns true or timeout
// This avoids hard-coded sleeps and makes the test resilient to timing variations
assert.Eventually(t, func() bool {
return counter == 1
}, 500*time.Millisecond, 10*time.Millisecond)
}
Pitfalls and compiler errors
The biggest trap is mixing up assert and require. assert logs an error and moves on. require stops the test immediately. If you use assert.NoError to check a database connection, and the connection fails, the assertion logs the error. The test continues. The next line tries to query the database with a nil connection. The program panics. The panic masks the original error. You see a stack trace about nil pointer dereference instead of "connection refused". Use require for setup steps. Use assert for independent checks.
Another common mistake is the argument order. assert.Equal takes t first, then expected, then actual. If you forget t, the compiler rejects the code with cannot use 4 as *testing.T value in argument. The type system catches this instantly. If you swap expected and actual, testify still works, but the error message might be confusing. The library prints "Expected: X, Actual: Y". If you passed them in the wrong order, the message is backwards. Stick to the convention: expected first, actual second.
Also, testify is a third-party library. It adds a dependency. Some teams prefer the standard library to keep dependencies minimal. The standard library approach is more verbose but requires no external packages. The trade-off is clear. You gain convenience and better output at the cost of one import. Most teams accept this trade-off. The time saved on debugging tests outweighs the dependency cost.
Decision matrix
Use testify/assert when you want to check multiple conditions in a single test and see all failures at once. Use testify/require when a failure makes the rest of the test meaningless, such as a failed setup or a nil pointer that will cause a panic. Use the standard testing package when you want zero external dependencies and don't mind writing if got != want { t.Fatal(...) } blocks. Use testify/mock when you need to replace external dependencies like databases or HTTP clients with controlled fakes.
Use assert to collect failures. Use require to protect the flow.