gomock Tutorial: Mocking in Go with gomock and mockery

gomock Tutorial: Mocking in Go with gomock and mockery

Mocking in Go is interface-based. You define an interface, generate a mock implementation, and use it in tests to control behavior and verify interactions. The two main tools are gomock (from the Google team) and mockery (a third-party code generator). This tutorial covers both.

Why Mock?

Pure unit tests isolate the code under test from dependencies. When your service calls a database, sends an email, or hits an external API, you don't want those side effects in unit tests. Mocks replace those dependencies with controllable fakes:

// Real service: hits the database
type UserRepository interface {
    FindByID(id int) (*User, error)
    Save(user *User) error
}

// In tests: use a mock that returns whatever you need

Mocks let you:

  • Test error handling without triggering real errors
  • Verify that methods were called with correct arguments
  • Test code that depends on infrastructure you can't control in tests

gomock Setup

Install:

go install go.uber.org/mock/mockgen@latest

(Note: the canonical gomock is now at go.uber.org/mock after Google transferred the project.)

go get go.uber.org/mock/gomock

Generating Mocks

Given this interface:

// user/repository.go
package user

type Repository interface {
    FindByID(id int) (*User, error)
    Save(user *User) error
    Delete(id int) error
}

Generate a mock:

mockgen -source=user/repository.go -destination=user/mocks/mock_repository.go -package=mocks

Or use the go:generate directive in your source file:

//go:generate mockgen -source=repository.go -destination=mocks/mock_repository.go -package=mocks

Run: go generate ./...

This creates mocks/mock_repository.go with a MockRepository struct.

Writing Tests with gomock

package user_test

import (
    "testing"
    "go.uber.org/mock/gomock"
    "github.com/myapp/user"
    "github.com/myapp/user/mocks"
)

func TestUserService_GetUser(t *testing.T) {
    ctrl := gomock.NewController(t)
    // ctrl.Finish() is called automatically in Go 1.14+ via t.Cleanup

    mockRepo := mocks.NewMockRepository(ctrl)

    // Set expectation
    expected := &user.User{ID: 1, Name: "Alice"}
    mockRepo.EXPECT().
        FindByID(1).
        Return(expected, nil)

    svc := user.NewService(mockRepo)
    got, err := svc.GetUser(1)

    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    if got.Name != "Alice" {
        t.Errorf("got name %q, want %q", got.Name, "Alice")
    }
}

ctrl.Finish() (or automatic cleanup) verifies all EXPECT() calls were satisfied.

Setting Expectations

Basic Expectation

mockRepo.EXPECT().FindByID(1).Return(&user.User{ID: 1}, nil)

The mock will:

  1. Expect FindByID to be called with argument 1
  2. Return the &User{ID: 1} and nil error
  3. Fail the test if FindByID is NOT called

Any Argument

import "go.uber.org/mock/gomock"

mockRepo.EXPECT().FindByID(gomock.Any()).Return(nil, ErrNotFound)

Argument Matchers

// Exact match
mockRepo.EXPECT().FindByID(42)

// Any value
mockRepo.EXPECT().FindByID(gomock.Any())

// Custom matcher
mockRepo.EXPECT().Save(gomock.AssignableToTypeOf(&user.User{}))

// Inline matcher
mockRepo.EXPECT().Save(gomock.Cond(func(u interface{}) bool {
    return u.(*user.User).Name != ""
}))

Call Counts

// Called exactly once (default)
mockRepo.EXPECT().FindByID(1).Return(user, nil).Times(1)

// Called exactly N times
mockRepo.EXPECT().Save(gomock.Any()).Return(nil).Times(3)

// Called at least once
mockRepo.EXPECT().Delete(gomock.Any()).Return(nil).AnyTimes()

// May or may not be called
mockRepo.EXPECT().FindByID(gomock.Any()).Return(nil, nil).MaxTimes(1)

Ordered Calls

gomock.InOrder(
    mockRepo.EXPECT().FindByID(1).Return(user, nil),
    mockRepo.EXPECT().Save(user).Return(nil),
    mockRepo.EXPECT().Delete(1).Return(nil),
)

This enforces that the three calls happen in the specified order.

Return Functions

For dynamic return values:

mockRepo.EXPECT().
    FindByID(gomock.Any()).
    DoAndReturn(func(id int) (*user.User, error) {
        if id == 0 {
            return nil, ErrNotFound
        }
        return &user.User{ID: id}, nil
    })

mockery: An Alternative Generator

mockery generates testify-compatible mocks with a simpler syntax:

go install github.com/vektra/mockery/v2@latest

Generate:

mockery --name Repository --output ./mocks

The generated mock uses testify/mock:

type MockRepository struct {
    mock.Mock
}

func (m *MockRepository) FindByID(id int) (*User, error) {
    args := m.Called(id)
    return args.Get(0).(*User), args.Error(1)
}

Using in tests:

func TestGetUser_NotFound(t *testing.T) {
    mockRepo := new(mocks.MockRepository)
    mockRepo.On("FindByID", 99).Return((*user.User)(nil), ErrNotFound)

    svc := user.NewService(mockRepo)
    _, err := svc.GetUser(99)

    assert.ErrorIs(t, err, ErrNotFound)
    mockRepo.AssertExpectations(t)
}

mockery + testify has a looser feel compared to gomock's strict compile-time verification, but the testify ecosystem integration is convenient if you're already using it.

gomock vs mockery

Feature gomock mockery
Interface EXPECT() fluent API On()/Return() testify-style
Argument matching Rich, typed matchers mock.Anything, custom matchers
Verification Auto on ctrl.Finish() Manual AssertExpectations(t)
Generation mockgen mockery
Integration Stand-alone Works best with testify

Choose gomock for strict behavior verification with compile-time safety. Choose mockery if you're already using testify and want consistent style.

Mocking Best Practices

Mock Only External Dependencies

Mock interfaces that represent:

  • Databases and repositories
  • External HTTP APIs
  • Message queues
  • Email senders
  • File systems

Don't mock your own business logic. If you're mocking a struct in the same package, the design likely needs refactoring.

Design for Testability

Interfaces should be at the consumer side, not the implementation:

// Good — the service defines what it needs
type UserService struct {
    repo UserGetter  // interface defined here
}

type UserGetter interface {
    GetByID(id int) (*User, error)
}

// Bad — interface mirrors the full repository
type FullRepository interface {
    GetByID(id int) (*User, error)
    Save(*User) error
    Delete(id int) error
    List(filter Filter) ([]*User, error)
    Count() int
    // ... 20 more methods
}

Smaller interfaces are easier to mock and indicate tighter design.

Keep Expectations Readable

Group setup, action, and assertion clearly:

func TestCheckoutOrder(t *testing.T) {
    ctrl := gomock.NewController(t)
    mockPayment := mocks.NewMockPaymentGateway(ctrl)
    mockInventory := mocks.NewMockInventoryService(ctrl)

    // Arrange: set expectations
    mockInventory.EXPECT().Reserve("SKU-123", 1).Return(nil)
    mockPayment.EXPECT().Charge(29.99).Return("txn_456", nil)

    svc := NewCheckoutService(mockPayment, mockInventory)

    // Act
    order, err := svc.Checkout("user-1", "SKU-123", 1, 29.99)

    // Assert
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    if order.TransactionID != "txn_456" {
        t.Errorf("wrong txn ID")
    }
}

Auto-Regenerating Mocks

Add a generate.go file at the package root:

// generate.go
package myapp

//go:generate mockgen -source=user/repository.go -destination=user/mocks/mock_repository.go -package=mocks
//go:generate mockgen -source=payment/gateway.go -destination=payment/mocks/mock_gateway.go -package=mocks

Regenerate all mocks after interface changes:

go generate ./...

Run this in CI to verify mocks are in sync with interfaces.

Read more