Integration Testing: Testing the API End to End with WebApplicationFactory
We cover integration testing: its difference from unit testing, what testing pieces together means, flowing a real request end to end with WebApplicationFactory, and using a separate test database.
In the previous article we learned unit testing: we tested the pieces of code one by one, in isolation. We faked dependencies with Moq and never touched the real database. This is a powerful approach, but it has a blind spot. Each of the pieces can work correctly on its own but break when they come together. This is exactly the gap integration testing closes. Today we talk about how to test the API as a whole, while a real request flows end to end.
The Difference Between Unit Testing and Integration Testing
Let's clearly separate the two types of tests. A unit test isolates a single piece from the others and tests it; it asks "does this method work correctly on its own?" An integration test tests the combination of pieces; it asks "do these pieces work correctly when they come together?" One looks at a single cell with a microscope, the other checks whether the organs work together.
An orchestra analogy clarifies the matter. A unit test is checking one by one that each musician plays their own instrument correctly. But even if each musician plays wonderfully on their own, they may not be in harmony when they all play together. An integration test listens to exactly that collectively-played performance: is the rhythm holding, are the sounds in harmony? In a real API too, even if the pieces (controller, service, database) are correct one by one, problems can arise at their junctions. An integration test catches this.
What Does an Integration Test Test?
In an integration test, a real HTTP request enters from the outermost part of the API and passes through all the layers: routing, model binding, validation, controller, service, database... Then you check the returned response. That is, you've tested not just a method, but the entire path a request follows from start to finish. This is the type of test that most closely imitates real usage.
Did you feel the difference? In a unit test we faked dependencies; in an integration test we use the real thing as much as possible. The aim is to set up conditions as close as possible to what will happen in production. Of course this has a cost: integration tests are slower than unit tests because more things actually run. So the two complement each other; many fast unit tests, fewer comprehensive integration tests.
WebApplicationFactory: Standing Up the Application for Testing
So don't we need to run the API somewhere to flow a real request? This is the tool .NET offers for this job: WebApplicationFactory. This class stands up the entire API in the test environment, in memory. There's no need for a real server, a real port; the application runs inside the test process, very close to the real thing.
Think of it like a flight simulator. To test the pilot, you don't fly a real plane; instead, you set up a simulator that behaves very close to the real thing. The pilot experiences real conditions there but in a safe and controlled environment. WebApplicationFactory sets up this simulator for your API too: the application really runs, processes real requests, but all inside the test environment, safely.
Writing the First Integration Test
This tool works in the test project we set up in the previous article. First we add the necessary test package:
dotnet add KullaniciApi.Tests package Microsoft.AspNetCore.Mvc.Testing
Then we write a test class. This class stands up the application using WebApplicationFactory and sends it real HTTP requests:
public class UsersEndpointTests
: IClassFixture>
{
private readonly HttpClient _client;
public UsersEndpointTests(WebApplicationFactory factory)
{
_client = factory.CreateClient();
}
[Fact]
public async Task GetAll_ReturnsSuccessStatusCode()
{
// Act
var response = await _client.GetAsync("/users");
// Assert
response.EnsureSuccessStatusCode();
}
}
Let's look step by step. IClassFixture<WebApplicationFactory<Program>> tells xUnit "stand up the application once for this test class and share it between tests." factory.CreateClient() gives us an HttpClient; a door through which we can send real requests to the application. Inside the test we make a real GET request to the /users address and, with EnsureSuccessStatusCode, check whether the response returns a successful status code (2xx). Notice: we faked nothing here; the request really passed through all the layers.
Note: Making the Program Class Visible
In the code above we wrote WebApplicationFactory<Program>; that is, the test needs to access your Program class. In the minimal hosting model, the Program class may be closed to the test project by default. To open this, you add a small line at the very bottom of the main project's Program.cs file:
public partial class Program { }
This single line makes the Program class visible to the test project. A small side effect of the minimal hosting model we discussed in the second article; once you're aware of it, the solution is simple.
Using a Separate Database for Testing
There's a critical matter: since an integration test passes through the real layers, it'll touch the database too. But you never want your tests to corrupt your real data. If a test adds and deletes a user, this should be in a test-specific place, not in your live data. So in tests we use a separate, isolated database.
The most common approach is to change the application's database setting during the test. WebApplicationFactory lets you customize this: while the application stands up, you connect a test-specific database (for example, an in-memory database set up from scratch in each test, or a separate test SQLite file) instead of the real database. This way each test starts with a clean slate and the tests don't affect each other. The configuration and environment logic we learned in the fifteenth article comes in handy here: giving different settings for the test environment feels very natural with that knowledge.
Using the Two Types of Tests Together
Let's clarify: integration testing doesn't replace unit testing. The two answer different questions and are powerful together. In practice, a balanced approach is: test the subtleties of the code's logic with fast unit tests; verify the combination of pieces and the real flow with fewer but comprehensive integration tests. Many small unit tests run in seconds and catch logic errors. Fewer integration tests give the assurance of "does it really work when everything comes together?" The balance of the two is the foundation of a solid testing culture.
A Small Experiment
Add an integration test to your test project with WebApplicationFactory and make a real request to an existing GET endpoint; check the returned status code. Then write a test for a POST endpoint: send a request that creates a user and verify both the status code and the returned data. If you like, go one step further and connect a separate database for testing, then write a test that adds a record and reads it back. This experiment clearly shows how different an assurance an integration test gives compared to a unit test: you're now testing not just the pieces, but the whole.
In the next article we move on to caching. We'll talk about how the API improves performance by temporarily storing frequently requested but rarely changing data instead of pulling it from the database every time: in-memory cache and distributed cache with Redis. We've secured correctness with tests; next is improving speed.