Mock interfaces

Mock interfaces in Go by creating a struct that implements the interface methods to control test behavior and isolate dependencies.

The dependency trap

You write a test for your user service. It passes on your laptop. It fails in CI because the staging database is being migrated. Or worse, it passes in CI but flakes randomly because an external API is slow. You didn't test your code. You tested the network. You tested the database availability. You tested the internet.

This is the dependency trap. Your code depends on things outside its control. When those things change, your tests break, even if your logic is perfect. Go solves this with interfaces. Mocking is the technique that makes interfaces useful in tests. You define a struct that pretends to be your dependency. You control exactly what it returns. You isolate your logic from the outside world.

Interfaces as contracts

An interface in Go is a list of methods. Any struct that has those methods satisfies the interface. There is no implements keyword. There is no registration. The compiler checks the methods, not the type name. This is structural typing.

Think of an interface as a socket. Any plug with the right pins fits. A real database is one plug. A mock is another plug. Your code only cares about the socket. It doesn't care what's behind the plug.

This design choice is deliberate. Go avoids heavy inheritance hierarchies and complex mocking frameworks. You don't need a tool to generate mocks. You write a struct. You add the methods. You inject it. The language features are sufficient.

Interfaces define the shape. Mocks fill the shape with predictable behavior.

The function field pattern

The simplest mock uses a function field. You define a struct with a field that holds a function. The method on the struct calls that function. This lets you change the behavior per test without creating a new struct every time.

Here is the simplest mock pattern: a struct with a function field.

// Greeter defines the behavior we want to mock.
type Greeter interface {
    Greet(name string) string
}

// RealGreeter implements Greeter using a real dependency.
type RealGreeter struct{}

func (g *RealGreeter) Greet(name string) string {
    // Simulate a slow or flaky external call.
    return "Hello, " + name + " from the real server"
}

// MockGreeter implements Greeter using a function field.
type MockGreeter struct {
    // GreetFunc holds the behavior for this test.
    GreetFunc func(name string) string
}

// Greet delegates to the function field.
func (m *MockGreeter) Greet(name string) string {
    // Call the injected function to return controlled data.
    return m.GreetFunc(name)
}

The MockGreeter struct has one field: GreetFunc. The Greet method just calls it. In your test, you set GreetFunc to return exactly what you want.

func TestService(t *testing.T) {
    // Create a mock with specific behavior for this test.
    mock := &MockGreeter{
        GreetFunc: func(name string) string {
            return "Hello, " + name + " from the mock"
        },
    }

    // Use the mock in your service.
    service := NewService(mock)
    result := service.Process("Alice")

    if result != "Hello, Alice from the mock" {
        t.Errorf("unexpected result: %s", result)
    }
}

This pattern is powerful because it is explicit. You see the behavior right in the test setup. There is no hidden state. There is no complex framework configuration.

Go checks the methods, not the type name. If it walks like a duck and quacks like a duck, the compiler treats it as a duck.

When you need to track calls

Sometimes you don't just need to return data. You need to verify that a method was called. Or you need to count how many times it was called. For this, you use a state-based mock. You add fields to track state, and the methods update that state.

Here is a mock that tracks call counts and arguments.

// UserStore defines the database interface.
type UserStore interface {
    Save(ctx context.Context, u User) error
}

// MockUserStore tracks calls to Save.
type MockUserStore struct {
    // SaveFunc defines the return value.
    SaveFunc func(ctx context.Context, u User) error
    // SavedUsers records every user passed to Save.
    SavedUsers []User
}

// Save updates state and delegates to the function.
func (m *MockUserStore) Save(ctx context.Context, u User) error {
    // Record the user for later verification.
    m.SavedUsers = append(m.SavedUsers, u)
    // Return the result defined by the test.
    if m.SaveFunc != nil {
        return m.SaveFunc(ctx, u)
    }
    return nil
}

In this mock, Save does two things. It appends the user to SavedUsers. It calls SaveFunc if it exists. This lets you verify both the outcome and the interaction.

func TestSaveUser(t *testing.T) {
    mock := &MockUserStore{
        SaveFunc: func(ctx context.Context, u User) error {
            return nil
        },
    }

    service := NewUserService(mock)
    err := service.Create(context.Background(), User{Name: "Bob"})
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }

    // Verify the mock was called with the right data.
    if len(mock.SavedUsers) != 1 {
        t.Error("expected one save call")
    }
    if mock.SavedUsers[0].Name != "Bob" {
        t.Error("expected user name Bob")
    }
}

This pattern is useful for verifying side effects. Did the service call the database? Did it pass the right context? Did it handle the error correctly? The mock records the truth.

Convention aside: context.Context always goes as the first parameter, conventionally named ctx. Functions that take a context should respect cancellation and deadlines. Your mock should accept the context even if it doesn't use it. This keeps the interface consistent with the real implementation.

Realistic example: HTTP handler and database

Real code often involves HTTP handlers, databases, and error handling. Here is a realistic scenario. An HTTP handler creates a user. It calls a service. The service saves the user to a database. You want to test the handler without a real database.

Here is the interface and the service.

// UserStore is the interface for the database.
type UserStore interface {
    Save(ctx context.Context, u User) error
}

// UserService handles business logic.
type UserService struct {
    Store UserStore
}

// Create saves a user and returns an error.
func (s *UserService) Create(ctx context.Context, u User) error {
    // Validate the user before saving.
    if u.Name == "" {
        return errors.New("name is required")
    }

    // Save the user.
    if err := s.Store.Save(ctx, u); err != nil {
        // Wrap the error to preserve context.
        return fmt.Errorf("save user: %w", err)
    }
    return nil
}

The service uses the UserStore interface. It doesn't know if it's a real database or a mock. It just calls Save. The error handling is verbose by design. The community accepts the boilerplate because it makes the unhappy path visible. if err != nil { return err } is the standard pattern.

Here is the mock and the test.

// MockUserStore implements UserStore for testing.
type MockUserStore struct {
    SaveFunc func(ctx context.Context, u User) error
}

func (m *MockUserStore) Save(ctx context.Context, u User) error {
    if m.SaveFunc != nil {
        return m.SaveFunc(ctx, u)
    }
    return nil
}

func TestUserService_Create(t *testing.T) {
    // Create a mock that returns an error.
    mock := &MockUserStore{
        SaveFunc: func(ctx context.Context, u User) error {
            return errors.New("database connection failed")
        },
    }

    service := &UserService{Store: mock}
    err := service.Create(context.Background(), User{Name: "Alice"})

    // Check that the error was wrapped correctly.
    if err == nil {
        t.Error("expected error, got nil")
    }
    if !strings.Contains(err.Error(), "database connection failed") {
        t.Errorf("unexpected error message: %s", err)
    }
}

The test creates a mock that returns a specific error. It injects the mock into the service. It calls the service. It checks the error. The test is fast. It is deterministic. It doesn't depend on a database.

Context is plumbing. Run it through every long-lived call site.

Pitfalls and compiler errors

Mocking is simple, but it has traps. The first trap is forgetting to implement all methods. If your interface has two methods, your mock must have two methods. If you miss one, the compiler rejects the program.

The compiler complains with cannot use mock (type MockStore) as type UserStore in argument: missing method Save. This error is clear. It tells you exactly what is missing. Fix the method and the error goes away.

The second trap is state leakage. If your mock stores state, that state persists between tests if you reuse the mock. Always create a new mock for each test. Or reset the state in a TestMain or t.Cleanup function.

The third trap is over-mocking. Don't mock the thing you are testing. If you are testing the service, mock the database. If you are testing the database query, use a real test database. Mocking too much hides bugs in your integration logic.

The fourth trap is using heavy frameworks. Tools like gomock or mockgen generate mocks for you. They are useful for large interfaces with many methods. For small interfaces, they add complexity. You don't need a code generator to write a struct. Write the struct. It is less code. It is easier to read.

A mock that requires more code to set up than the real dependency is a design smell.

Decision matrix

Choosing the right test strategy depends on what you are testing. Use the right tool for the job.

Use a mock when you need to verify interactions or control return values for edge cases. Use a mock when the dependency is slow, flaky, or requires external setup. Use a mock when you want to test error handling without triggering real failures.

Use a real test database when you need to test SQL queries, transactions, or schema migrations. Use a real database when the logic depends on database-specific behavior like indexes or constraints.

Use a fake implementation when the dependency is cheap to run but slow. Use a fake when you can simulate the behavior with in-memory data structures.

Use no mock when the function is pure and has no dependencies. Use no mock when the function only uses standard library functions that are deterministic.

Mock the dependency, not the logic.

Where to go next