Testing with Mocks in Go

Learn how to use mock objects in Go tests to isolate and test units of code effectively

Using mock objects is an essential practice in unit testing that allows you to isolate the code under test, ensuring that external dependencies do not affect the test results. In Go, the gomock library from Google is a popular choice for implementing mock objects.

Setting Up gomock

Install gomock and the mockgen tool with the following command:

go get github.com/golang/mock/gomock
go install github.com/golang/mock/mockgen

Basic Example Using gomock

Below is an example of using gomock to create and use a mock object in a test. Suppose we have an interface DataStore:

package main

type DataStore interface {
    GetData(key string) (string, error)
}

We want to test a function FetchData that relies on an implementation of DataStore:

package main

type DataStore interface {
    GetData(key string) (string, error)
}

func FetchData(store DataStore, key string) (string, error) {
    return store.GetData(key)
}

Generating the Mock

Generate a mock for the DataStore interface:

mockgen -destination=mocks/mock_datastore.go -package=mocks . DataStore

Writing a Test with Mocks

Here's how to use the generated mock to test FetchData:

package main

import (
    "testing"
    "github.com/golang/mock/gomock"
    "your_project/mocks"
)

func TestFetchData(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    // Create a mock instance.
    mockStore := mocks.NewMockDataStore(ctrl)

    // Define behavior - Expectation.
    mockStore.EXPECT().GetData("key123").Return("value123", nil)

    // Call the function.
    data, err := FetchData(mockStore, "key123")
    if err != nil {
        t.Fatalf("expected no error, got %v", err)
    }

    if data != "value123" {
        t.Errorf("expected value123, got %v", data)
    }
}

Best Practices

  • Align Expectations: Clearly specify expectations on mocks to match the actual usage within your tests.
  • Finish Controllers: Ensure to call Finish() on every mock controller to verify all expectations have been met.
  • Use Sub-tests: Leverage sub-tests for different scenarios to maintain organized and readable test code.
  • Minimal Mocks: Mock only the necessary methods for each test to reduce complexity.

Common Pitfalls

  • Over-mocking: Avoid mocking too many dependencies which can lead to brittle tests.
  • Expectation Drift: If a method's signature changes, ensure all mock expectations are updated to avoid runtime panics.
  • Opaque Error Messages: Use descriptive error messages in expectations to quickly diagnose failing tests.

Performance Tips

  • Targeted Mocks: Mock only what's necessary for the specific unit of code being tested.
  • Parallel Tests: Run tests in parallel where possible to reduce overall test execution time.
  • Mock Reuse: Reuse mock setups across similar tests to avoid redundant code and reduce setup time.

Implementing mock objects in Go using the gomock library can greatly increase the robustness and reliability of your unit tests by isolating dependencies and focusing purely on the logic you're testing.