Blog

Patterns for Mocking APIs

Dec 9, 2024 | 8 minutes read

In a cloud-native or microservice architecture it is common to have a business layer service that uses a client to communicate with an external REST API to fetch data or perform some action on a remote system.

When it comes time to write unit tests for the business service we often need to mock that remote API in order to drive the tests. There are a number of ways to approach this, each with benefits and drawbacks. This post explores some patterns for mocking downstream APIs within unit or integration tests.

Note: Code samples for this blog are available in this github repository.

The samples are written in Java, but the principles can be applied in many languages.

To illustrate the patterns in this post we will use a very simple (and somewhat contrived) example of a Users and Groups API that is called within a business service via an API client.

The example structure

The business service performs some business logic based on the result of the API call, and it is this business logic we want to test in our unit tests.

public class MyService {
    private final UserGroupsClient client;

    public MyService(UserGroupsClient client) {
        this.client = client;
    }

    /**
     * This is a silly example that represents a service-layer
     * operation that performs some business logic with the response
     * from the API client.
     *
     * The details of this logic are not interesting for this blog
     * post except that it is this logic we want to exercise in our
     * unit tests.
     */
    public List<String> listUsernamesInGroup(long groupId) {
        try {
            return client.getGroup(groupId)
                .users()
                .stream()
                .map(User::username)
                .toList();
        } catch (NotFound nfe) {
            return List.of();
        }
    }
}

To test our business service logic there are several patterns we can apply. This post describes four patterns. Each pattern has its own benefits and drawbacks, and generally increase in sophistication, expressiveness and maintainability. The most suitable pattern to use will depend on your specific use case.

The “simplest” pattern is to directly mock the client the business service uses, either by writing your own implementation or by using a library like mockito.

Mocking the API client

This approach gives complete control over the client behavior within the unit tests, which can be useful if the client is performing complex logic like data transformation or handling pagination.

@ExtendWith(MockitoExtension.class)
public class TestWithMockedClient {

    @Mock
    private UserGroupsClient client;

    @InjectMocks
    private MyService service;

    @Test
    public void listUsernamesInGroup_returnsUserIds_whenNonEmptyGroup() {
        // Setup
        when(client.getGroup(eq(1000L))).thenReturn(
                new Group(1000, "group_1000", List.of(
                        new User(101, "user101", "user101@example.com"),
                        new User(102, "user102", "user102@example.com")
                ))
        );

        // Execute
        final var result = service.listUsernamesInGroup(1000);

        // Assertions
        assertThat(result).contains("user101", "user102");
    }
}

However, it suffers from the problem that your mocked client behavior can include incorrect assumptions about the remote system behavior, which can lead to integration errors and bugs. It is most effective if you also have tests in place for the client itself along with its interactions with the remote API.

Use this pattern when:

  • Your client performs complex logic like handling pagination or data transformation
  • Your API client is tested independently of your business service

If you want to test the unit that encompasses the business service and the API client you can mock interactions with the external API at the HTTP level.

Mocking the API calls

The most basic way to do this is to mock the interactions in-line within the unit tests you write, using a tool like wiremock. Each test sets up the HTTP interactions it expects and mocks the responses it needs to drive the test case.

/**
 * Example test using wiremock to mock the HTTP interactions with the remote API
 */
public class TestWithMockedCalls {

    @RegisterExtension
    static WireMockExtension wm = WireMockExtension.newInstance()
            .options(wireMockConfig().dynamicPort())
            .build();

    private MyService service;

    @BeforeEach
    public void setup() {
        final var client = new Client(wm.baseUrl());
        this.service = new MyService(client);
    }

    @Test
    public void listUsernamesInGroup_returnsUserIds_whenNonEmptyGroup() {
        // Setup
        final var apiResponse = new Group(1000, "group_1000", List.of(
                new User(101, "user101", "user101@example.com"),
                new User(102, "user102", "user102@example.com")
        ));
        wm.stubFor(get("/group/1000").willReturn(okForJson(apiResponse)));

        // Execute
        final var result = service.listUsernamesInGroup(1000);

        // Mock
        assertThat(result).contains("user101", "user102");
    }
}

This approach has the benefit of driving tests with API responses which removes a source of integration error, but suffers from the problem that low-level HTTP logic is pushed into unit tests that should be focused on higher-level business logic. As your test suite grows you tend to find that this HTTP mocking logic gets repeated multiple times, and making changes across multiple mocked calls becomes brittle.

Use this pattern when:

  • You want to test the business service + client as a unit
  • You have simple interactions with the external API
  • You only have a few unit tests to drive

One of the main problems of Pattern 2 is that you end up repeating a lot of low-level mocking setup thoughout your tests. This mocking code gets in the way of expressing the intent of the test and often becomes brittle as the external API evolves and requires changes to the mocks. This becomes especially problematic if the same external API is being mocked in multiple test classes.

A way to help mitigate these problems is to centralize the mocking logic into a single place. This lets you re-use the mocking logic and hide the implementation details.

Mocking the API centrally

One approach is to move mocking logic to a series of helper methods, with naming that indicates what is being set up:

/**
 * Example of a test using a helper method to setup the API mocking
 */
@Test
public void listUsernamesInGroup_returnsUserIds_whenNonEmptyGroup() {
    // Setup
    MockHelper.whenGroupApiReturnsAGroup(
        new Group(1000, "group_1000", List.of(
            new User(101, "user101", "user101@example.com"),
            new User(102, "user102", "user102@example.com")
        ))
    );

    // Execute
    final var result = service.listNamesInGroup(1000)

    // Check
    assertThat(result).contains("user101", "user102");
}

This works fine for an API with a small surface area, but as you start trying to mock more operations and cover more scenarios the number of helper methods grows and you start getting awkward method names like whenGroupApiReturnsNotFoundForGetGroup().

You can mitigate this by modelling the mocking to reflect the structure of the remote API:

api.<resource>.<operation>.<behavior>

This centralizes the mocking and gives a fluent way to express it that in turn raises the level of abstraction within your tests.

/**
 * Example of a test using a mocked API that reflects the structure of the remote API
 */
public class TestWithMockApi {

    @RegisterExtension
    static MockUserGroupApi api = new MockUserGroupApi();

    private MyService service;

    @BeforeEach
    public void setup() {
        final var client = new Client(api.baseUrl());
        this.service = new MyService(client);
    }

    @Test
    public void listUsernamesInGroup_returnsUserIds_whenNonEmptyGroup() {
        // Setup
        api.groupResource()
            .getGroup()
            .returnsSuccessWith(
                new Group(1000, "group_1000", List.of(
                    new User(101, "user101", "user101@example.com"),
                    new User(102, "user102", "user102@example.com")
                ))
            );

        // Execute
        final var result = service.listUsernamesInGroup(1000);

        // Check
        assertThat(result).contains("user101", "user102");
    }
}

Use this pattern when:

  • You want to raise the level of abstraction in your tests
  • You need to mock the same external API in multiple test classes
  • You have an exteral API with a large surface area to mock

Pattern 3 provides a clean and maintanable way to mock an external API. The main remaining problem is that there is nothing to verify that the mocking reflects the actual behavior of the remote API. It is easy to introduce an invalid mocking that renders the unit tests invalid, either through an incorrect assumption about the API behavior or from something as simple as a typo.

To “close the loop” you can apply contract testing techniques to verify the mocks either against the remote service itself (using something like Pact) or against the API specification (using something like swagger-request-validator). This helps give confidence that the mocks you are using to drive your unit tests reflect the actual service behavior.

Mocking the API with validation

In the “mocked API” approach of Pattern 3 this becomes a simple matter of applying validation within the mocked API class.

/**
 * An example of applying API spec validation to the mocked API.
 * This becomes a drop-in replacement for your existing tests.
 * You get API validation with no changes to test logic.
 */
public class ValidatedMockUserGroupApi extends WireMockExtension {

    private static final OpenApiValidator API_VALIDATOR = new OpenApiValidator("/api.yml");

    public ValidatedMockUserGroupApi() {
        super(newInstance()
                .options(wireMockConfig()
                .dynamicPort()
                .extensions(API_VALIDATOR))
        );
    }

    @Override
    protected void onAfterEach(final WireMockRuntimeInfo wireMockRuntimeInfo) {
        API_VALIDATOR.assertValidationPassed();
    }

    ...
}

Note: You can apply API validation and contract testing to Pattern 2 in a similar way, but having a mocked API class gives a convenient central place to make changes without having to refactor tests.

Use this pattern when:

  • You want to “close the loop” and ensure your mocks reflect reality
  • You want to detect when changes to the external API break your usage

This post presents four patterns that can be applied when mocking an external API in your unit tests:

  1. Mocking the client - suitable when the client is generated and/or tested on its own
  2. Mock the HTTP calls within tests - suitable when you have a small number of tests and/or a simple API
  3. Centralize the mocked HTTP calls - suitable when the API is consumed in multiple places and/or the API surface area is large
  4. Validate the HTTP mocks - suitable when you want to “close the loop” and ensure your mocks reflect reality

The patterns can be applied in any language, and which you choose will depend on your situation. Happy coding!