The best way to test Web APIs
The art of writing unit tests for Web APIs to achieve maximal confidence
Sponsor of the week: Multiplayer
Multiplayer auto-documents your system, from the high-level logical architecture down to the individual components, APIs, dependencies, and environments.
Perfect for teams who want to speed up their workflows and consolidate their technical assets.
Thank you to our sponsors who keep this newsletter free to the reader! If you want to sponsor us, click here.
Motivation
Over the past years, I figured out a powerful approach to test my Web APIs. It enables me to write high-level tests that cover a wide feature range of my application while maintaining fast test execution. This approach results in high confidence in my code and also supports my Test-Driven Development (TDD) flow, where running my tests often is essential.
In this post, I will show you how to efficiently test your Web APIs with two different approaches:
Solitary vs Sociable API tests
There are two kinds of unit tests we can use for Web APIs:
Solitary Unit Test: it completely isolates the tested unit from its collaborators with the heavy use of mocks. For a WebAPI, it would involve completely separating the HTTP layer from the rest of the system
Sociable Unit Test: the tested unit relies on other components (classes, modules) to fulfill the behavior. For a WebAPI, it would be a full-stack test that covers the entire flow, from the HTTP request, through the business logic, to persisting data in a fake database.
They both come with different unit sizes and testing strategies. Let’s examine each through a real-life API example.
Example API + Test server
Let’s imagine we need to test the following /product endpoint:
To test HTTP endpoints, we need to spin up web servers. However, starting a fully-fledged web server is resource-intensive, which is why it's more efficient to use an in-memory HTTP server for testing. This allows you to send requests and write tests without the overhead of a real server.
Here are a few tools for each language that can help you achieve that:
C# - Microsoft.AspNetCore.TestHost
Java - MockMvc (part of the Spring Test framework)
Python - Flask-Testing
JavaScript - Supertest
Go - httptest (part of the Go standard library)
Sociable Unit Test
By default, especially for single web services where the HTTP API serves as the sole entry point, I prefer sociable unit tests. These tests promote better realism and confidence, make refactoring easier, and also provide high-level documentation for the application.
Here's how it works in C#:
First, we set up an in-memory test server
Then, we use a client to send an HTTP POST request to this server, with the body containing product details.
Finally, we assert the expected HTTP status code and check if the product was correctly stored in the database:
Solitary Unit Test
However, for more complex applications that involve multiple clients and UI elements interacting with my domain, I favor solitary unit tests for my WebAPIs. My aim is to separate the HTTP layer from other parts of my application. This approach also has several advantages: the tests run faster, and the API conventions can be tested without having the business logic implemented.
The main disadvantage is that it relies on mocks and results in tight coupling to my underlying IProductService
, specifically to its CreateAsync
method:
Summary
The rule is simple: use sociable unit tests by default. They provide better realism and give you higher confidence in your test suite.
Use solitary unit tests only if you need to delay decisions or isolate a specific part of your application for testing purposes.
Happy Testing!
How Can I Help You?
There are two ways I could help you:
Transform Your Craft With TDD: Join 200+ engineers and master Test-Driven Development. You’ll not just learn TDD; you’ll master the best practices to produce quality software via real-world projects.
Promote Yourself to 23,000+ subscribers by sponsoring this newsletter.
Great article!, Daniel.
In the past, I had a bug that took me days to reproduce, and it finally only occurred when the entire scope was used.
Since then, I do end-to-end tests for happy paths using Superset with a staging or in-memory database.
I use sociable tests starting from the controllers, testing the cases more exhaustively and mocking the data layer with test doubles.
Regards
Great article to understand the difference between solitary and sociable tests.