Mocking in Go with testify/mock: A Practical Guide
Mocking in Go requires more boilerplate than in dynamic languages — you have to implement an interface manually or generate the implementation. The testify/mock package makes this manageable without reflection magic or code generation requirements. This guide covers the full workflow: defining mocks, setting expectations, asserting calls, and using mockery to automate the tedious parts.
Why Mocking in Go Is Different
Go mocks are interface implementations. If your code depends on a concrete type, it can't be mocked. This forces a good habit: depend on interfaces, not implementations.
// Not mockable
type UserService struct{}
func (s *UserService) GetUser(id int) (*User, error) { ... }
// Mockable
type UserGetter interface {
GetUser(id int) (*User, error)
}
// Your handler depends on the interface
type Handler struct {
users UserGetter
}Once you have an interface, testify/mock can replace the real implementation in tests.
Installing testify
go get github.com/stretchr/testifyThe testify module includes assert, require, mock, and suite packages. You only need to import what you use.
Writing a Mock Manually
A mock struct embeds mock.Mock and implements the interface:
// The interface we want to mock
type EmailSender interface {
Send(to, subject, body string) error
}
// The mock implementation
type MockEmailSender struct {
mock.Mock
}
func (m *MockEmailSender) Send(to, subject, body string) error {
args := m.Called(to, subject, body)
return args.Error(0)
}m.Called(...) records that the method was called and returns the values you configured with On(). For error returns, use args.Error(N) — it handles nil correctly (unlike args.Get(N).(error) which panics on nil).
For non-error return values:
type UserGetter interface {
GetUser(id int) (*User, error)
}
type MockUserGetter struct {
mock.Mock
}
func (m *MockUserGetter) GetUser(id int) (*User, error) {
args := m.Called(id)
// args.Get(0) returns interface{} — type assert carefully
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*User), args.Error(1)
}The nil check before type assertion is critical. If you return nil for the *User and do args.Get(0).(*User) directly, you'll panic.
Setting Expectations with mock.On()
func TestWelcomeEmail(t *testing.T) {
sender := &MockEmailSender{}
// Set up expectation: Send will be called with these exact args
sender.On("Send", "alice@example.com", "Welcome", "Hi Alice").Return(nil)
svc := NewUserService(sender)
err := svc.WelcomeNewUser("alice@example.com", "Alice")
assert.NoError(t, err)
sender.AssertExpectations(t)
}AssertExpectations(t) fails the test if any expected call wasn't made. It's the core assertion — always call it at the end of tests that set up expectations.
Argument Matchers
Exact argument matching can be brittle. Use matchers for flexibility:
import "github.com/stretchr/testify/mock"
// Match any string
sender.On("Send", mock.AnythingOfType("string"), mock.Anything, mock.Anything).Return(nil)
// Match any value of any type
sender.On("Send", mock.Anything, mock.Anything, mock.Anything).Return(nil)
// Custom matcher function
sender.On("Send",
mock.MatchedBy(func(email string) bool {
return strings.HasSuffix(email, "@example.com")
}),
mock.Anything,
mock.Anything,
).Return(nil)Use exact values when the specific argument matters to the behavior you're testing. Use matchers when you only care that the method was called with the right type or shape.
Return Values and Errors
// Return success
getter.On("GetUser", 1).Return(&User{ID: 1, Name: "Alice"}, nil)
// Return an error
getter.On("GetUser", 999).Return(nil, errors.New("user not found"))
// Return different values on successive calls
getter.On("GetUser", 1).Return(&User{ID: 1}, nil).Once()
getter.On("GetUser", 1).Return(nil, errors.New("cache miss")).Once()
// Use a function to compute return values dynamically
getter.On("GetUser", mock.Anything).Return(func(id int) *User {
return &User{ID: id, Name: fmt.Sprintf("User %d", id)}
}, nil)The .Once() modifier makes an expectation valid for exactly one call. .Times(n) for a specific count. Without a modifier, the expectation matches unlimited times.
AssertExpectations and AssertNumberOfCalls
// Assert all On() expectations were called
sender.AssertExpectations(t)
// Assert a specific method was called N times
sender.AssertNumberOfCalls(t, "Send", 2)
// Assert a method was called with specific args
sender.AssertCalled(t, "Send", "alice@example.com", mock.Anything, mock.Anything)
// Assert a method was NOT called
sender.AssertNotCalled(t, "Send")Testing Error Paths
Mocks make error path testing straightforward — you control exactly what the dependency returns:
func TestGetUserHandlesError(t *testing.T) {
getter := &MockUserGetter{}
getter.On("GetUser", 42).Return(nil, errors.New("database unavailable"))
handler := NewUserHandler(getter)
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/users/42", nil)
handler.ServeHTTP(w, r)
assert.Equal(t, http.StatusInternalServerError, w.Code)
getter.AssertExpectations(t)
}Without mocks, triggering a database error in a test requires either a real database in a broken state or complex test setup. With a mock, it's one line.
Using mockery for Code Generation
Writing mock structs manually gets tedious for large interfaces. mockery generates them:
go install github.com/vektra/mockery/v2@latest
# Generate mock for a specific interface
mockery --name=UserGetter --dir=./internal/users --output=./internal/mocks
# Generate all interfaces in a package
mockery --all --dir=./internal/users --output=./internal/mocksThe generated mock is identical to what you'd write manually, but you don't have to maintain it. Add a go:generate comment to keep it in sync:
//go:generate mockery --name=UserGetter
type UserGetter interface {
GetUser(id int) (*User, error)
}Then run go generate ./... to regenerate all mocks.
Organizing Mocks
Common approaches:
Option 1: mocks/ subdirectory — put generated mocks in internal/mocks/. Clean separation, but imports get long.
Option 2: _test.go files — define mocks in the same package, in a _test.go file. They're only compiled for tests and live close to the code.
Option 3: Separate testutil package — a shared package that multiple test files import. Useful when the same mock is needed across packages.
For most projects: put generated mocks in mocks/ at the package level and don't overthink it.
A Complete Example
// payment/processor.go
package payment
type PaymentGateway interface {
Charge(amount int, currency, token string) (string, error)
Refund(chargeID string, amount int) error
}
type Processor struct {
gateway PaymentGateway
}
func NewProcessor(gateway PaymentGateway) *Processor {
return &Processor{gateway: gateway}
}
func (p *Processor) Purchase(amount int, token string) (string, error) {
if amount <= 0 {
return "", errors.New("amount must be positive")
}
return p.gateway.Charge(amount, "USD", token)
}// payment/processor_test.go
package payment_test
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type MockGateway struct {
mock.Mock
}
func (m *MockGateway) Charge(amount int, currency, token string) (string, error) {
args := m.Called(amount, currency, token)
return args.String(0), args.Error(1)
}
func (m *MockGateway) Refund(chargeID string, amount int) error {
args := m.Called(chargeID, amount)
return args.Error(0)
}
func TestPurchase(t *testing.T) {
t.Run("successful charge", func(t *testing.T) {
gw := &MockGateway{}
gw.On("Charge", 1000, "USD", "tok_123").Return("ch_abc", nil)
p := NewProcessor(gw)
chargeID, err := p.Purchase(1000, "tok_123")
assert.NoError(t, err)
assert.Equal(t, "ch_abc", chargeID)
gw.AssertExpectations(t)
})
t.Run("gateway error", func(t *testing.T) {
gw := &MockGateway{}
gw.On("Charge", 1000, "USD", "tok_bad").Return("", errors.New("card declined"))
p := NewProcessor(gw)
chargeID, err := p.Purchase(1000, "tok_bad")
assert.Error(t, err)
assert.Empty(t, chargeID)
gw.AssertExpectations(t)
})
t.Run("invalid amount skips gateway", func(t *testing.T) {
gw := &MockGateway{}
// No On() call — we expect Charge to NOT be called
p := NewProcessor(gw)
_, err := p.Purchase(-1, "tok_123")
assert.Error(t, err)
gw.AssertNotCalled(t, "Charge")
})
}Common Mistakes
Not calling AssertExpectations — your mock can be set up with On() but never called, and the test still passes without this assertion.
Type asserting nil — args.Get(0).(*User) panics when args.Get(0) is nil. Always nil-check first.
Matching too loosely — mock.Anything everywhere means you're not testing what arguments your code passes. Use exact values for arguments that matter.
Not using interfaces — if you find yourself unable to mock a dependency, the fix is usually to extract an interface, not to work around the concrete type.
testify/mock gives you enough control to test every code path without hitting real external services. Combined with mockery for generation, the boilerplate cost drops significantly.
HelpMeTest adds a layer on top by monitoring your real deployed service end-to-end, catching the integration gaps that even well-mocked unit tests miss.