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:

bash
mock: Expected calls that were not fulfilled:
        GetUser(string)
                0: "user-123"
--- FAIL: TestCreateUser (0.00s)
FAIL

Or with AssertExpectations:

bash
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() or Times(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.Anything sparingly -- specific argument matching catches more bugs
  • Run tests with -race flag to detect concurrent mock access issues