Introduction

The testify/mock package provides mock objects for Go testing, but mock assertions fail when argument types do not match exactly, when expected call counts differ from actual calls, or when concurrent goroutines call the mock in unexpected orders. The most common failures are mock: Unexpected Method Call when the mock was not set up for the given arguments, and assert.Equal failures when comparing complex types that do not implement equality. Understanding testify's argument matching rules and using mock.MatchedBy for flexible matching are essential for reliable mock-based tests.

Symptoms

``` mock: Unexpected Method Call -----------------------------

GetUser(int) 0: 42

Diff: --- Expected +++ Actual @@ -1,2 +1,2 @@ -[]interface {}{ - (int) 42, +[]interface {}{ + (int64) 42, ```

Or:

bash
Expected: 1 actual: 0
        at: /app/user_test.go:45

Common Causes

  • Type mismatch in arguments: int vs int64, string vs fmt.Stringer
  • Method not set up on mock: Called method not recorded with On()
  • Wrong call count expected: Method called more or fewer times than AssertNumberOfCalls expects
  • Concurrent calls not synchronized: Mock called from multiple goroutines
  • Pointer vs value receiver: Mock expects pointer but gets value
  • Return value type mismatch: Mock returns wrong type for the call

Step-by-Step Fix

Step 1: Match argument types correctly

```go type MockUserRepo struct { mock.Mock }

func (m *MockUserRepo) GetUser(id int64) (*User, error) { args := m.Called(id) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*User), args.Error(1) }

// Test func TestGetUser(t *testing.T) { mockRepo := &MockUserRepo{}

// IMPORTANT: Use int64, not int expectedUser := &User{ID: 42, Name: "Alice"} mockRepo.On("GetUser", int64(42)).Return(expectedUser, nil)

// Now the call matches user, err := mockRepo.GetUser(42) assert.NoError(t, err) assert.Equal(t, expectedUser, user)

// Verify call count mockRepo.AssertNumberOfCalls(t, "GetUser", 1) mockRepo.AssertCalled(t, "GetUser", int64(42)) } ```

Step 2: Use MatchedBy for flexible argument matching

```go import "github.com/stretchr/testify/mock"

func TestCreateUser(t *testing.T) { mockRepo := &MockUserRepo{}

// Match any User with name starting with "test" mockRepo.On("CreateUser", mock.MatchedBy(func(u *User) bool { return u != nil && u.Name != "" && u.Email != "" })).Return(&User{ID: 1}, nil)

err := mockRepo.CreateUser(&User{ Name: "test_user", Email: "test@example.com", }) assert.NoError(t, err)

mockRepo.AssertExpectations(t) } ```

Step 3: Handle concurrent mock calls

```go func TestConcurrentAccess(t *testing.T) { mockRepo := &MockUserRepo{} mockRepo.Test(t) // Enable test mode for concurrent safety

// Set up mock to handle multiple concurrent calls user := &User{ID: 1, Name: "test"} mockRepo.On("GetUser", mock.AnythingOfType("int64")). Return(user, nil). Times(10) // Expect exactly 10 calls

var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func(id int64) { defer wg.Done() _, err := mockRepo.GetUser(id) assert.NoError(t, err) }(int64(i)) }

wg.Wait() mockRepo.AssertExpectations(t) } ```

Prevention

  • Use exact types in mock expectations -- int64 not int, pointer not value
  • Use mock.MatchedBy for complex argument matching that does not need exact equality
  • Call mock.AssertExpectations(t) at the end of every test
  • Use mock.Test(t) to enable concurrent-safe mock behavior
  • Verify call counts with AssertNumberOfCalls for critical method invocations
  • Set up mock expectations before the code under test runs
  • Use mock.Anything or mock.AnythingOfType("string") when argument value does not matter