Introduction
The testify/mock package in Go provides a fluent interface for setting up mock expectations and verifying call counts. When a mock method is called a different number of times than expected, the test fails with mock: Unexpected Method Call or AssertExpectations failure. This error is common in unit tests that verify interaction patterns between components -- such as ensuring a database method is called exactly once, or a notification service is not called on failure. Understanding how testify tracks call counts and how to set proper expectations is essential for reliable mock-based tests.
Symptoms
``` === RUN TestCreateUser mock.go:321: assert: mock: Unexpected Method Call -----------------------------------
GetUser(string) 0: "user-123"
The closest call I have is:
GetUser(string) 0: "user-123"
Difference: No difference, but called 2 times, expected 1 times --- FAIL: TestCreateUser (0.00s) ```
Or:
mock: Expected calls that were not fulfilled:
GetUser(string)
0: "user-123"
--- FAIL: TestCreateUser (0.00s)
FAILOr with AssertExpectations:
FAIL: GetUser(string="user-123")
at: [/app/user_service_test.go:45]Common Causes
- Method called more times than expected: Loop or retry logic calls the mock multiple times
- Method not called at all: Code path does not reach the expected call
- Argument mismatch: Call happens but with different arguments than expected
- Mock not reset between tests: Shared mock instance accumulates calls across tests
- Times() not specified: Default expectation allows unlimited calls
- Concurrent calls to mock: Multiple goroutines call the mock, causing race on call counter
Step-by-Step Fix
Step 1: Set explicit call count expectations
```go func TestCreateUser_CallsRepositoryOnce(t *testing.T) { repo := new(MockUserRepository)
// Expect exactly one call repo.On("GetUser", "user-123"). Return(&User{ID: "user-123", Name: "John"}, nil). Once()
// Or specify exact count repo.On("SendNotification", "user-123"). Return(nil). Times(1)
svc := NewUserService(repo) err := svc.CreateUser(context.Background(), "user-123") assert.NoError(t, err)
// Verify all expectations were met repo.AssertExpectations(t) } ```
Step 2: Assert call counts after the fact
```go func TestCreateUser_CallCounts(t *testing.T) { repo := new(MockUserRepository) repo.On("GetUser", mock.Anything).Return(&User{}, nil) repo.On("SaveUser", mock.Anything).Return(nil) repo.On("SendNotification", mock.Anything).Return(nil)
svc := NewUserService(repo) err := svc.CreateUser(context.Background(), "user-123") assert.NoError(t, err)
// Verify exact call counts repo.AssertNumberOfCalls(t, "GetUser", 1) repo.AssertNumberOfCalls(t, "SaveUser", 1) repo.AssertNumberOfCalls(t, "SendNotification", 1)
// Verify no unexpected calls repo.AssertNotCalled(t, "DeleteUser") } ```
Step 3: Handle retry logic with variable call counts
```go func TestCreateUser_WithRetry(t *testing.T) { repo := new(MockUserRepository)
// First call fails, second succeeds repo.On("GetUser", "user-123"). Return((*User)(nil), errors.New("timeout")). Once() repo.On("GetUser", "user-123"). Return(&User{ID: "user-123"}, nil). Once() repo.On("SaveUser", mock.Anything).Return(nil)
svc := NewUserService(repo) err := svc.CreateUserWithRetry(context.Background(), "user-123") assert.NoError(t, err)
// Should have been called twice (initial + 1 retry) repo.AssertNumberOfCalls(t, "GetUser", 2) repo.AssertExpectations(t) } ```
Step 4: Reset mocks between tests
```go func setupMocks(t *testing.T) *MockUserRepository { t.Helper() repo := new(MockUserRepository) t.Cleanup(func() { repo.AssertExpectations(t) }) return repo }
func TestCreateUser(t *testing.T) { repo := setupMocks(t) repo.On("GetUser", "user-123").Return(&User{ID: "user-123"}, nil) repo.On("SaveUser", mock.Anything).Return(nil)
// Test logic... } ```
Prevention
- Always use
repo.AssertExpectations(t)at the end of tests - Use
Once()orTimes(n)for methods that should be called a specific number of times - Use
t.Cleanup()to ensure expectation verification runs even on test failure - Create fresh mock instances per test -- never share mocks across tests
- Use
mock.Anythingsparingly -- specific argument matching catches more bugs - Run tests with
-raceflag to detect concurrent mock access issues