How to Use Testify for Assertions and Mocking in Go

Use Testify's assert package for readable checks and the mock package to simulate dependencies in Go tests.

The friction of manual testing

You write a function that calculates tax. You test it. You add a dependency on a database. Now you need a fake database. You write a fake struct. You add a method. You realize you need to check if the method was called. You add a counter. You realize you need to check the arguments. You add a slice of calls. Your test code is longer than the production code. The test is fragile. One signature change breaks five test files.

This is the friction that Testify solves. Testify is the most popular testing library in the Go ecosystem. It does not replace the standard testing package. It sits on top of it, providing helpers for assertions and a framework for mocking dependencies. The standard library gives you t.Error and t.Fail. Testify gives you assert.Equal and mock.On. You still write tests with func TestXxx(t *testing.T). The test runner remains the same. Testify just makes the test body less noisy and the mocks less tedious.

Assertions and mocks in plain words

Testify has two main components. The assert package provides functions to check values. Instead of writing if got != want { t.Errorf(...) }, you write assert.Equal(t, want, got). The mock package provides a way to create fake implementations of interfaces. You embed mock.Mock in a struct, and the framework records calls, checks arguments, and returns configured values.

Think of assert as a strict grader. You hand in the answer, and the grader tells you if it's right. Think of mock as a stunt double. Your production code expects a database. You give it a stunt double that pretends to be a database. The stunt double knows exactly what to say when asked a question, and it keeps a log of every question it was asked.

Minimal assertion example

Here's the simplest assertion: import the package, pass the testing.T, and compare values.

package main

import (
	"testing"

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

// Add returns the sum of two integers.
func Add(a, b int) int {
	return a + b
}

// TestAdd checks that Add produces the correct result.
func TestAdd(t *testing.T) {
	// Pass t to assert so failures report the right line number.
	// assert.Equal compares the expected value first, then the actual.
	// The order is (t, expected, actual).
	assert.Equal(t, 4, Add(2, 2))
}

The assert package functions take *testing.T as the first argument. This allows the assertion to call t.Errorf internally when a check fails. The test continues running after an assert failure. This is useful when you want to see all failures in a single test run. If you need the test to stop immediately, use the require package instead. require.Equal calls t.FailNow on failure.

The convention is to use assert for most checks and require for setup. If a setup step fails, the rest of the test is meaningless. Stopping early prevents cascading errors and confusing output.

Assert continues. Require stops. Pick the one that matches your risk.

How assertions work under the hood

When you call assert.Equal, the function compares the values. If they differ, it formats a message and calls t.Errorf. The message includes the expected value, the actual value, and the file and line number. Testify does not use reflection to hide details. It uses standard Go comparison logic.

You can add a custom message as the last argument. assert.Equal(t, 4, Add(2, 2), "Add should sum integers"). The message appears in the output if the assertion fails. This helps when the values alone don't explain the context.

Testify provides many assertion helpers. assert.NoError checks for nil errors. assert.Contains checks if a slice or string contains a value. assert.ElementsMatch checks if two slices have the same elements regardless of order. These helpers reduce boilerplate. They also standardize error messages across your test suite.

Run gofmt on your test files. Test code is code. The community expects consistent formatting in tests just like in production.

Realistic mocking example

Mocking requires an interface. Go does not have reflection-based mocking. You must define an interface and implement it with a mock struct. The mock struct embeds mock.Mock. Embedding gives the struct all the methods from mock.Mock for free.

Here's the interface and the mock definition.

package service

import (
	"github.com/stretchr/testify/mock"
)

// UserRepository defines the data access contract.
// Define interfaces where you use them, not where you implement them.
type UserRepository interface {
	FindByID(id int) (User, error)
}

// User represents a domain entity.
type User struct {
	ID   int
	Name string
}

// MockUserRepository embeds mock.Mock to enable expectation tracking.
// Embedding gives the struct all the methods from mock.Mock for free.
type MockUserRepository struct {
	mock.Mock
}

// FindByID implements the interface method using the mock framework.
// The implementation delegates to mock.Mock to handle call recording.
func (m *MockUserRepository) FindByID(id int) (User, error) {
	// Called records the invocation and matches arguments against expectations.
	// It returns an Arguments object containing the configured return values.
	args := m.Called(id)
	// Extract return values by index. Get(0) returns the first return value.
	// Type assertion is needed because mock stores values as interface{}.
	return args.Get(0).(User), args.Error(1)
}

The receiver name is m, matching the type MockUserRepository. This follows the Go convention of short receiver names. The FindByID method calls m.Called(id). This records the call and looks up the expectation. The expectation is configured in the test using On. The On method takes the method name, the arguments, and the return values.

Here's the test that uses the mock.

package service

import (
	"testing"

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

// UserService depends on the repository interface.
type UserService struct {
	repo UserRepository
}

// GetUser retrieves a user by ID from the repository.
func (s *UserService) GetUser(id int) (User, error) {
	return s.repo.FindByID(id)
}

// TestGetUser demonstrates setting up expectations and verifying calls.
func TestGetUser(t *testing.T) {
	// Instantiate the mock. new() returns a pointer to a zero-value struct.
	mockRepo := new(MockUserRepository)
	// On configures a specific call: method name, arguments, and return values.
	// The mock will return these values when FindByID(1) is invoked.
	mockRepo.On("FindByID", 1).Return(User{ID: 1, Name: "Alice"}, nil)

	service := &UserService{repo: mockRepo}
	user, err := service.GetUser(1)

	// Assert the outcome matches expectations.
	assert.NoError(t, err)
	assert.Equal(t, "Alice", user.Name)

	// AssertExpectations checks that all configured On calls were actually made.
	// This ensures the code under test didn't skip the dependency.
	mockRepo.AssertExpectations(t)
}

The test creates a mock instance. It configures an expectation with On. When FindByID(1) is called, the mock returns the configured user. The test asserts the result. Finally, AssertExpectations verifies that the mock was called as expected. This catches cases where the code under test skips the dependency call.

Accept interfaces, return structs. The service accepts UserRepository and returns User. This pattern makes testing possible. If the service accepted a concrete struct, you couldn't inject the mock.

Advanced mocking: matchers and arguments

Exact argument matching works for simple types. When arguments are complex, use mock.MatchedBy. This allows you to define a predicate function that returns true for valid arguments.

// TestMatchedBy shows how to assert on complex arguments.
func TestMatchedBy(t *testing.T) {
	mockRepo := new(MockUserRepository)
	// MatchedBy uses a function to validate arguments instead of exact equality.
	// This is useful when the argument is a struct with many fields or a pointer.
	mockRepo.On("FindByID", mock.MatchedBy(func(id int) bool {
		return id > 0
	})).Return(User{ID: 42, Name: "Bob"}, nil)

	service := &UserService{repo: mockRepo}
	user, _ := service.GetUser(42)

	assert.Equal(t, "Bob", user.Name)
	mockRepo.AssertExpectations(t)
}

The MatchedBy function receives the argument and returns a boolean. The mock matches the call if the function returns true. This is useful for testing logic that depends on argument properties rather than exact values.

If your interface takes a context.Context, your mock must take a context. Testify does not skip parameters. The On call must include the context argument. You can use mock.Anything to match any value, including contexts. mock.Anything is a wildcard matcher.

Pitfalls and runtime errors

Testify introduces a few pitfalls. The most common is using assert for setup. If assert fails during setup, the test continues. The next line might panic because a required value is nil. Use require for setup. require.NoError stops the test if the error is not nil.

Another pitfall is forgetting AssertExpectations. If you configure a mock with On but the code under test never calls the method, the test passes unless you call AssertExpectations. Always call AssertExpectations at the end of the test. The worst mock bug is the one that never logs.

Mock return types must match the interface. If the interface returns (User, error), the Return call must provide a User and an error. If you return the wrong type, the type assertion in the mock method will panic. The runtime panics with panic: interface conversion: interface {} is string, not User. The compiler does not catch this because Return takes ...interface{}. You must ensure the types match manually.

If you call a method on the mock that is not configured with On, the mock panics with mock: Unexpected Method Call. This helps catch missing configurations. You can disable this behavior by calling mock.Test(nil) or using mock.Called without setup, but the default panic is safer. It forces you to define expectations explicitly.

Don't mock the thing you're testing. Mocks are for dependencies. If you mock the logic inside the function, you're testing the mock, not the code. Keep mocks at the boundaries.

Decision matrix

Use assert when you want the test to continue running after a failure to collect multiple errors in one pass. Use require when a failure makes the rest of the test meaningless, such as a missing setup value or a failed database connection. Use mock when you need to isolate a unit from slow or external dependencies like databases, HTTP clients, or file systems. Use the standard t.Errorf when the test is trivial and adding a dependency for a single check adds more noise than value.

Mocks are contracts. If your mock breaks, your interface changed. Trust the interface.

Where to go next