Skip to content
gohacks.dev
Testing

Testify Mocks With Expectations and Argument Matchers

Write testify mocks for Go interfaces: embed mock.Mock, use On and Return, match arguments with Anything and MatchedBy, assert expectations and read failures.

11 min read
Summarize in AI
Flat illustration of a test report with one failing test.

Testify's mock package lets you build test doubles by hand-with-help: you write a small struct per interface, and the package records calls, matches arguments and checks expectations. It sits between a hand-written fake and a generated mock, and a lot of Go code uses it. This article shows how it works on Go 1.25.6 with testify v1.11.1, what its failures look like, and where it gets in your way.

Tested with Go 1.25.6, testify v1.11.1, macOS on Apple Silicon (arm64)

The mock and the code under test

mock_test.goGo
package main

import (
	"errors"
	"strings"
	"testing"

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

type Mailer interface{ Send(to, body string) error }

type MockMailer struct{ mock.Mock }

func (m *MockMailer) Send(to, body string) error {
	args := m.Called(to, body)
	return args.Error(0)
}

func Welcome(m Mailer, email string) error { return m.Send(email, "welcome") }

func TestWelcomeSends(t *testing.T) {
	m := new(MockMailer)
	m.On("Send", "[email protected]", "welcome").Return(nil).Once()
	assert.NoError(t, Welcome(m, "[email protected]"))
	m.AssertExpectations(t)
	m.AssertNumberOfCalls(t, "Send", 1)
	t.Logf("calls recorded: %d", len(m.Calls))
}

func TestMatchersAndErrors(t *testing.T) {
	m := new(MockMailer)
	m.On("Send", mock.MatchedBy(func(s string) bool { return len(s) > 5 }), mock.Anything).Return(errors.New("smtp down"))
	err := Welcome(m, "[email protected]")
	t.Logf("err=%v", err)
}

func TestUnexpectedCallPanics(t *testing.T) {
	m := new(MockMailer)
	m.On("Send", "[email protected]", "welcome").Return(nil)
	defer func() {
		r := recover()
		msg, _ := r.(string)
		t.Logf("panicked: %v, says unexpected method call: %v", r != nil, strings.Contains(msg, "Unexpected Method Call"))
	}()
	_ = Welcome(m, "[email protected]")
}

func TestMissingCallFailsExpectations(t *testing.T) {
	m := new(MockMailer)
	m.On("Send", "[email protected]", "welcome").Return(nil)
	// Welcome is never called, so the expectation is not met.
	inner := &testing.T{}
	ok := m.AssertExpectations(inner)
	t.Logf("expectations met: %v, test marked failed: %v", ok, inner.Failed())
}

func main() {}
Shell
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*$'
Text
    calls recorded: 1
--- PASS: TestWelcomeSends
    err=smtp down
--- PASS: TestMatchersAndErrors
    panicked: true, says unexpected method call: true
--- PASS: TestUnexpectedCallPanics
    expectations met: false, test marked failed: true
--- PASS: TestMissingCallFailsExpectations

The interface is Mailer with one method. MockMailer embeds mock.Mock and implements Send in two lines: it calls m.Called(to, body), which records the call, finds a matching expectation and returns its configured values, then reads the first return value as an error with args.Error(0). That pattern is the same for every method: pass all arguments to Called, then read the return values by position with args.Error(i), args.String(i), args.Int(i), args.Get(i) for anything else, and args.Bool(i).

Welcome is the code under test; it sends a fixed message.

Setting expectations

m.On("Send", "[email protected]", "welcome").Return(nil).Once() registers an expectation: when Send is called with exactly these arguments, return nil, once. The pieces are:

  • the method name as a string, which must match the method that calls Called;
  • the arguments, compared with ObjectsAreEqual, or matchers;
  • Return(...) with the values the method should give back, in order;
  • optional modifiers: Once(), Twice(), Times(n), Maybe(), After(d), Run(func(args mock.Arguments){...}), WaitUntil(ch).

After the code runs, m.AssertExpectations(t) checks that every expectation was satisfied, and m.AssertNumberOfCalls(t, "Send", 1) checks the count. The lab records calls recorded: 1, from len(m.Calls), the list of calls the mock keeps. That list is available for custom checks: m.Calls[0].Arguments.String(1).

Argument matchers

Exact arguments are often too strict. The package has matchers:

  • mock.Anything matches any value.
  • mock.AnythingOfType("string") matches by type name.
  • mock.MatchedBy(func(s string) bool {...}) matches by a predicate; the function's parameter type decides which arguments it accepts.
  • mock.IsType(&User{}) matches by dynamic type.

TestMatchersAndErrors registers m.On("Send", mock.MatchedBy(func(s string) bool { return len(s) > 5 }), mock.Anything).Return(errors.New("smtp down")). [email protected] satisfies the predicate, the body is anything, and Welcome returns smtp down. This is the right tool for arguments that carry noise, such as generated IDs or timestamps: assert the property you care about and let the rest through.

Use mock.Anything sparingly. A test where every argument is Anything verifies only that the method was called, which is a weak statement. The strength of a mock test is in the arguments.

Failures

Two failure modes matter, and they behave differently.

An unexpected call panics. TestUnexpectedCallPanics sets an expectation for [email protected] and calls Welcome with [email protected]. No expectation matches, so Called panics with a message that starts mock: Unexpected Method Call, followed by the call and the closest expectation. The test recovered it to check the text; in a normal test the panic fails the test, with the message in the output. The panic is deliberate. The mock cannot return a value for a call it was not told about, and a silent default would hide the mismatch.

A missing call fails AssertExpectations. TestMissingCallFailsExpectations registers an expectation and never calls the method. AssertExpectations returns false and marks the test failed: the log shows expectations met: false, test marked failed: true. The lab passes it a throwaway &testing.T{} so the outer test can report on it; in your own tests pass the real t. Forgetting to call AssertExpectations is the usual mistake. A mock with expectations that is never verified is a mock that cannot fail for missing calls.

A tidy pattern is defer m.AssertExpectations(t) right after creating the mock, or mock.Test(t) plus the NewXxx(t) constructors that mockery generates, which register the assertion with t.Cleanup.

Hand-writing versus generating

The mock in the lab is hand-written. Each method needs the same boilerplate, and a large interface means a lot of it. The mockery tool generates these structs from an interface, including typed EXPECT() helpers that replace the string method names with real function calls, so that a renamed method fails to compile. For a small interface, writing the mock by hand is quick. For a big one, generate.

When a mock is the wrong tool

Mocks check interactions: "this method was called with these arguments". That is useful at a boundary where the interaction is the behaviour, such as sending an email once. It is a poor fit where state is the behaviour. A test that mocks a repository to return a canned user and asserts the service called Save with the right fields is checking the implementation. A fake repository, a map in memory, would let the test assert on the resulting state instead, and survive a refactor that changes how many calls are made. The trade-offs are covered in fakes, stubs and mocks.

Signs that you are over-mocking:

  • Tests have more setup lines than assertions.
  • Changing an internal call order breaks tests whose behaviour did not change.
  • Every collaborator of the unit is mocked, including pure helpers.
  • The mock returns data that the test then asserts on directly.

Specific traps

  • Method name typos. m.On("Sned", ...) compiles and fails at run time with a message about an unexpected call.
  • Argument type mismatch. On("Get", 1) does not match a call with int64(1); ObjectsAreEqual is type-strict. Use mock.Anything or the right type.
  • Pointer arguments. Pointers are compared by ObjectsAreEqual, which looks at contents for most types, but a pointer to a type with unexported fields may compare unexpectedly. Prefer MatchedBy for pointers.
  • Return counts. args.Error(0) when Return() was given nothing panics with an index error. Match the number of returns to the method signature.
  • Concurrency. The mock is safe to call from several goroutines, but expectations are consumed in order; with Once() and parallel callers the winner is arbitrary.
  • Reusing a mock across tests. Expectations accumulate. Create a fresh mock per test.

Return values, side effects and Run

Return sets what the method gives back. When the mocked method needs to do something besides return, Run hooks in: m.On("Save", mock.Anything).Run(func(args mock.Arguments) { u := args.Get(0).(*User); u.ID = 7 }).Return(nil). This is how you simulate a repository that assigns an ID or fills a field on the pointer it was given. It is powerful and easy to overuse; every Run is code inside a test double, and it can contain bugs of its own.

For methods that should return different things on successive calls, register several expectations with .Once(), in order. The first matching unconsumed expectation answers each call. A pattern for retries: the first Send returns an error once, the second returns nil, and the test asserts the code called Send twice. Use Times(n) for a repeated answer, and leave off the modifier for "any number of times", which also makes AssertExpectations pass if the method is never called, so add AssertCalled or AssertNumberOfCalls when the count matters.

Checking what was called, after the fact

Expectations are set before the run. The mock also records calls so you can check afterwards, which reads more like an ordinary test:

  • m.AssertCalled(t, "Send", "[email protected]", "welcome") passes if a matching call happened.
  • m.AssertNotCalled(t, "Send", mock.Anything, mock.Anything) passes if the method never ran, useful for "does not send when the email is invalid".
  • m.AssertNumberOfCalls(t, "Send", 2) checks the count.
  • m.Calls is the raw record, each entry with the method name and Arguments.

The after-the-fact style needs the method to be registered with On anyway, because an unregistered call panics. A common arrangement is m.On("Send", mock.Anything, mock.Anything).Return(nil) as a permissive default, then assertions on Calls at the end. It keeps the setup short and puts the checks together with the other assertions.

Order of calls

Testify mocks do not check the order of calls on one mock, unless you use .NotBefore(...) (available in recent versions) to declare that one expectation must come after another. Order is an implementation detail in most code, and a test that insists on it breaks on harmless refactors. Check order only when it is part of the contract: a transaction must begin before the insert, a lock must be taken before the write. Even then, a fake that records an event log and a test that compares the log is often clearer than a chain of expectations.

Mocks compared with fakes

Testify mockHand-written fake
SetupPer test, with On and ReturnOnce, in a type
VerifiesCalls and argumentsState, if you assert on it
Failure on unexpected callPanic with a messageWhatever the fake does
Compile-time checkingNone for method names or argumentsFull
Survives refactoringPoorly if call patterns changeBetter, if it models behaviour
Cost for large interfacesBoilerplate per methodBoilerplate once, shared

A fake pays back when many tests use the same collaborator. A mock pays back when a few tests need to check one specific interaction. Many codebases use both: a fake repository for most service tests and a mock mailer for the one test that asserts the welcome email goes out exactly once.

Moving to generated mocks

If you keep writing MockX structs, generate them. mockery produces testify-compatible mocks from interfaces and, with the EXPECT() API, replaces On("Send", ...) with m.EXPECT().Send(...), so a renamed method fails to compile. I describe mockery from its documentation and did not run it on Go 1.25, as noted in the gomock and mockery article. The migration is mechanical: delete the hand-written struct, run the generator, replace On calls with EXPECT() calls. Keep the tests' structure and see whether they still say what the code does.

Testing code that fails halfway

Mocks shine for error paths, which real collaborators make hard to reach. A mailer that fails on the second call, a repository that returns sql.ErrNoRows, a payment client that times out: each is one Return(err) away. Write a test per failure mode your code claims to handle, and assert what the code does about it. For Welcome, the question is whether it wraps the error with context, whether it retries and whether it logs. Three small tests with three different Return values document the policy better than a paragraph of comments.

For a timeout, return context.DeadlineExceeded and check that the function passes it up unchanged, so callers can use errors.Is. For a transient error followed by success, register two Once() expectations and assert the retry happened by counting calls. Keep sleeps out of it by injecting the wait, as in the retry testing article.

Concurrency and the mock

The mock guards its internal state with a mutex, so calling it from several goroutines is safe. What is not deterministic is which goroutine matches which Once() expectation. If a test runs workers in parallel and each calls the mock with different arguments, make the expectations argument-specific so they match regardless of order. When the code under test starts goroutines, wait for them before calling AssertExpectations, or the assertion races with the calls it is checking; the race detector will report it. Use a WaitGroup or a channel the mock's Run hook closes.

Quick checklist for a mock-based test

  1. A fresh mock per test.
  2. Expectations set before the code runs, specific enough to mean something.
  3. AssertExpectations(t) called, preferably with defer.
  4. The behaviour under test visible in an assertion on the result, not only on the calls.
  5. Failures reachable: at least one test per error the code handles.
  6. No logic in Run hooks beyond what a fake would need.

Testify's own documentation of the mock package lists every call modifier and matcher, and the Go blog's pieces on testing in general are collected from the testing package overview.

A balanced rule

Use testify/mock when the interaction is the contract, the interface is small and the team already uses testify. Use a hand-written fake otherwise, and a real component when you can afford it. Verify expectations every time. The package is documented in the mock reference, and the broader practice in the assertions tutorial and the testing guide.

Tagged""
Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.