Mocking Dependencies with Testify in TDD

Introduction to Mocks

As we progress in our understanding of isolating dependencies with test doubles, in this lesson we introduce mocks, powerful tools for simulating external dependencies in software tests. In previous lessons, talked about Dummies and Stubs, which allow you to put something in the place of a dependency. However, mocks allow us to replicate the behavior of complex systems, enabling testing and verification in isolation without dependence on unpredictable systems like databases or web services.

Adhering to the TDD workflow remains crucial:

  • Red: Start with a failing test.
  • Green: Implement just enough code to pass the test.
  • Refactor: Clean up your code without altering its functionality.

Using Go and Testify, we'll demonstrate how mocks are applied to effectively isolate and test application logic.

Why Use Mocks in TDD?

Mocks are essential in TDD, as they allow you to test code independently of system parts you don't control. When constructing tests for a PricingService, for instance, you might mock an external currency conversion service to avoid failures due to downtime or unexpected changes in the API. Mocks create a controlled environment, where various conditions and responses can be simulated and calls can be validated.

Unlike stubs that merely return data, mocks fully simulate dependencies by preventing the actual code or functionality from executing and provide validation that the mock was called. For example, if a function interacts with an external API, a mock could simulate that API's response without making any network requests and verify the API was called with specific parameters.

Mocking Fundamentals with Testify

We'll now explore mocking fundamentals using Testify. We'll initiate by understanding how to create mocks and set up expectations in Go.

Consider the ExchangeRateService, responsible for fetching exchange rates from an API. When testing the PricingService, mocking this service ensures that tests do not depend on real API interactions.

Below is an example using Testify's mock package for setting up a test:

package pricing_test

import (
    "testing"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/mock"
)

type MockExchangeRateService struct {
    mock.Mock
}

func (m *MockExchangeRateService) GetRate(fromCurrency, toCurrency string) (float64, error) {
    args := m.Called(fromCurrency, toCurrency)
    return args.Get(0).(float64), args.Error(1)
}

func TestConvertPrice_ShouldUseExchangeRate(t *testing.T) {
    // Arrange
    mockService := new(MockExchangeRateService)
    pricingService := PricingService{ExchangeRateService: mockService}

    // Mock out the result
    mockService.On("GetRate", "USD", "EUR").Return(1.5, nil)

    // Act
    result, err := pricingService.ConvertPrice(100, "USD", "EUR")

    // Assert
    assert.NoError(t, err)
    assert.Equal(t, 150.0, result)
    
    // VERIFY the mock was called
    mockService.AssertCalled(t, "GetRate", "USD", "EUR")
}

In this structure:

  • We define IExchangeRateService as an interface.
  • MockExchangeRateService is created using Testify's mock package, imitating the behavior without implementing actual logic.
  • mockService.On() sets up the expectation and return value for the method.
Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal