Setting up Integration Tests with External APIs

Advanced Test Setup Part 2 - External APIs

In this post I want to show you how to deal with external APIs in your tests. Though I will take a REST API as an example, I did use the same approach for SOAP APIs in the past, and it should also work for other protocols (gRPC, GraphQL, etc.).

The Problem

Your SUT (System Under Test) calls an external API. Sometimes (though rarely) you want your tests to execute against the real API.

This is usually the case for E2E tests and system integration tests (tests involving multiple systems).

But for unit tests and integration tests (tests involving only your system), you will want to replace the external API with a test double.

Mocking Tools

There are a number of tools you can use to set up a mock server.

I used WireMock and WireMock.Net in the past, and for many use cases they work well.

They work by spinning up a local server that listens on a port and can be configured to respond to requests in a deterministic way. Taking WireMock as an example, your test can set up the server to respond to a GET request to /api/users/1 with a 200 OK status code and a JSON body containing user data:

//Arrange
var wireMockServer = WireMock.Server.WireMockServer.Start();
wireMockServer.Given(Request.Create()
    .UsingGet()
    .WithPath("/users")
).RespondWith(Response.Create()
    .WithStatusCode(HttpStatusCode.OK)
    .WithBody([ "{ \"id\": 1, \"name\": \"Test User\" }" ]));

// Act
var response = sut.GetUsers();

// Assert
response.Should().HaveCount(1);
response[0].Name.Should().Be("Test User");

For simple scenarios, this works well. Especially for external APIs you only read from, and with only simple, straightforward requests, this is a good solution.

Let's look at another example with a POST request:

//Arrange
var wireMockServer = WireMock.Server.WireMockServer.Start();
wireMockServer.Given(Request.Create()
    .UsingPost()
    .WithPath("/users")
    .WithBody("{ \"name\": \"New User\" }")
).RespondWith(Response.Create()
    .WithStatusCode(HttpStatusCode.Created);

// Act
var response = sut.CreateUser(new User { Name = "New User" });

// Assert
???

How would you assert that the user was created correctly? If the API returns the created user, that's fine - you can assert on the response. But if, like in the code above, the API only returns a status code, then you are left with only one option: you have to check the mock server.

That is, after all, what a mock is: it records interactions and offers verifications. But, as I wrote in another blog post, this ties your test code to the implementation against the API. Tests become brittle, etc.

The second problem with WireMock (and similar tools) arises when your scenarios get more complex. I ran into this exact situation in one of my projects, where I worked as Lead Test Engineer for a small team of testers:

We set up WireMock for an external REST API our system integrated with. For the first twenty or so tests, everything worked fine. Then the scenarios became more complex:

  • We needed the external API to simulate what happened if the same request was sent multiple times
  • Some of our test cases involved both write and read operations to the API, with responses changing depending on the data written to it
  • A lot of tests used similar, but slightly different, test data

At this point, the test setup code became a real headache. We first tried to move the setup code into a test data module, which helped a bit. But then I tried to write the setup for a test that simulated >10 interactions with the API and I got confused. I did not understand the code I was writing. And if you do not understand the code at the time of writing it, there is no way someone else will be able to maintain it later.

So I scrapped the whole thing...

Writing a Fake API

... and wrote a faked API instead.

This is surprisingly easy to do. Especially if you have, for example, an OpenAPI specification to work from. There are tools that can generate a server stub from an OpenAPI spec.

Once you have a stub running, you can implement a simplified version of the real thing. And I do mean: simplified!

Do not attempt to re-implement the whole external system.

Instead of a real database, use a Mapping (Dictionary in C#). Instead of complex logic, create simplified responses.

Similar to the fake data repository I wrote about in my previous post, you can keep track of write operations by updating entities in a collection. On read operations, you can return entities from the collection.

For endpoints that would create results from calculations or aggregations, you can often just return fictitious data.

Similar to the data repository example, you can then interact with your fake API in your test setup like you would with a real API:

// Arrange
apiClient.CreateCompany(CreateTestCompanyRequest(id: companyId)); // the apiClient is the real client, but points to your fake API listening to a local port
apiClient.CreateUser(CreateTestUserRequest(companyId: companyId));

// Act
var userReport = sut.CreateUserReportForCompany(companyId);

// Assert
userReport.ActiveUsers.Should().Be(1);

Now the test is mostly decoupled from the implementation of the SUT against the external API. The test does not care how the GET requests against the API look like.

If you need to, you can always still extend the external API fake to record requests, respond with error codes, etc. But, honestly, I never needed to do that. I tend to have my unit tests handle error scenarios, and it is simple to mock or fake an IApiClient interface, as needed.

Fakes also have another advantage, which I want to mention briefly at the end: If you need your API to interact with third systems (shared databases, other APIs, message queues, etc.), then you will find it a LOT easier to extend your fake than you would using a mocking tool.

Conclusion

  • For simple scenarios, tools like WireMock work well.
  • When scenarios get more complex, consider writing a fake API (and use auto-creation tools if possible).
  • Keep the fake API simple - do not try to re-implement the whole external system.

If you have any questions or comments, please let me know! (Contact information is in the footer at the end of the page)

And if you have a project where you need help setting up tests, please reach out as well! I would be happy to help.

Until the next post: Happy Faking!