Skip to Content
Course content

78: Mocking Interfaces for Tests

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You've probably heard that to do "real" mocking in Go, you need a heavy-duty framework like GoMock or Testify. There's a common belief that unless you're using a tool that auto-generates mocks, your tests are just "amateur hour" or too manual to maintain. I'll be honest: I see this all the time with developers coming from Java or C#. They want a magic @Mock annotation that handles everything for them.

The Myth: Mocking Frameworks are Mandatory for Interface Tests

The problem with relying on a framework too early is that it obscures what's actually happening. If you try to mock a concrete struct—say, a DatabaseConnection struct—you'll quickly find that Go simply doesn't let you "override" methods on a struct the way you can in some other languages. You can't just tell a struct to behave differently during a test.

// This is what people TRY to do, but it doesn't work
type EmailSender struct {}
func (e *EmailSender) Send(msg string) error { 
    // actually sends an email... we don't want this in tests!
    return nil 
}

// You can't "intercept" this call without an interface.

If you try to force this, you'll end up with brittle tests or a codebase cluttered with "test-only" flags inside your production logic. That's a recipe for a nightmare.

The Reality: Interfaces are the Mock

In Go, mocking isn't a "feature" provided by a library; it's a natural consequence of how interfaces work. If your logic depends on an interface rather than a concrete type, you can pass in anything that satisfies that interface. I personally prefer "manual mocks" for 90% of my work because they are explicit and don't require a code-generation step every time I add a method.

Let's look at a real-world scenario: a PaymentProcessor that needs to talk to an external API (like Stripe or PayPal). We don't want to hit the actual API every time we run go test.

type PaymentGateway interface {
    Charge(amount int) error
}

type CheckoutService struct {
    Gateway PaymentGateway
}

func (s *CheckoutService) CompletePurchase(amount int) error {
    if amount <= 0 {
        return errors.New("invalid amount")
    }
    return s.Gateway.Charge(amount)
}

Now, instead of reaching for a library, we just build a small struct in our _test.go file. Here is the trick I use: I add a function field to the mock struct. This allows me to define the behavior per test case rather than creating five different mock structs for five different scenarios.

type MockGateway struct {
    ChargeFunc func(amount int) error
}

// This makes MockGateway satisfy the PaymentGateway interface
func (m *MockGateway) Charge(amount int) error {
    return m.ChargeFunc(amount)
}

func TestCompletePurchase_Success(t *testing.T) {
    mock := &MockGateway{
        ChargeFunc: func(amount int) error {
            return nil // Simulate success
        },
    }
    
    service := CheckoutService{Gateway: mock}
    err := service.CompletePurchase(100)
    
    if err != nil {
        t.Errorf("Expected success, got %v", err)
    }
}

func TestCompletePurchase_ApiFailure(t *testing.T) {
    mock := &MockGateway{
        ChargeFunc: func(amount int) error {
            return errors.New("api timeout") // Simulate failure
        },
    }
    
    service := CheckoutService{Gateway: mock}
    err := service.CompletePurchase(100)
    
    if err == nil || err.Error() != "api timeout" {
        t.Errorf("Expected api timeout error, got %v", err)
    }
}

See what happened there? By using a function field (ChargeFunc), we've created a flexible, lightweight mock without adding a single external dependency to our go.mod. It's type-safe, it's fast, and any engineer reading your code can see exactly what the mock is doing without having to learn a specific framework's DSL (Domain Specific Language).




📋 Practical Task

Simulating a Flaky External Weather API

You are building a WeatherAlertService that checks the temperature via an external API and returns a warning if it's too cold. You need to test how your service handles a "Service Unavailable" error from the API without actually making network calls.

Your Task:

  • Define a WeatherClient interface with a method GetTemperature(city string) (int, error).
  • Create a WeatherAlertService struct that takes a WeatherClient and has a method IsFreezing(city string) (bool, error). This method should return true if the temperature is 0 or below, and false otherwise.
  • In your test file, create a MockWeatherClient using the function-field pattern.
  • Write a test case TestIsFreezing_ApiError that forces the mock to return a errors.New("503 Service Unavailable") and asserts that the WeatherAlertService correctly bubbles that error up.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.