An integration test checks that multiple pieces of the application work together.
Unit tests are fantastic for verifying business logic (2 + 2 = 4). But a web API is much more than isolated logic.
An API has:
- A Routing system that must find the correct URL.
- A Model Binder that must parse JSON.
- Validation that must reject incorrect data.
- A Database that must save and read real information.
What good is perfect logic if the controller fails to serialize the response or if the SQL query is poorly written?
This is where Integration Tests come in.
Instead of testing an isolated class, we will spin up the complete API in memory, send a real HTTP request, and see what it responds.
The key tool: WebApplicationFactory
Microsoft provides the Microsoft.AspNetCore.Mvc.Testing package.
This library contains the WebApplicationFactory<T> class, which is capable of:
- Starting your API in the background (in memory, without opening a browser or real ports).
- Giving you a pre-configured
HttpClientto call that in-memory API.
Project setup
In your test project (MiApi.Tests), install the package:
dotnet add package Microsoft.AspNetCore.Mvc.TestingMake Program.cs visible
Since .NET 6, the Program generated by top-level statements remains internal. So the test project can see it and start the API, add a partial class at the end of your API’s Program.cs (not the test’s):
var app = builder.Build();
// ... configuration ...
app.Run();
public partial class Program { }Writing the first integration test
Let’s test a GET /api/productos endpoint. We want to ensure it returns a 200 status code and a list of products.
using Microsoft.AspNetCore.Mvc.Testing;
using Xunit;
// We implement IClassFixture so the API is started once
// and reused between tests (for performance).
public class ProductosIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public ProductosIntegrationTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
}
[Fact]
public async Task GetProductos_ReturnsSuccessAndJson()
{
// 1. Arrange: We create an HTTP client that "lives" in the test
var client = _factory.CreateClient();
// 2. Act: We call the real endpoint
var response = await client.GetAsync("/api/productos");
// 3. Assert
response.EnsureSuccessStatusCode(); // Verifies it's 2xx
var responseString = await response.Content.ReadAsStringAsync();
Assert.Contains("Keyboard", responseString); // We verify content
}
}You just tested routing, the controller, dependency injection, and the JSON response, all at once.
The test database
The previous test has a danger: it is using the real database configured in your appsettings.json.
If you run the test, you could modify development or production data.
For integration tests, we must replace the real database with a disposable one.
Replacing services with ConfigureWebHost
We can inherit from WebApplicationFactory and override the configuration to swap the real DbContext for a test one. For simple examples, you can use EF Core’s InMemory; if you want to check real SQL, use SQLite in-memory or Testcontainers.
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
public class MiApiFactory : WebApplicationFactory<Program>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
// Replace the SQL Server DbContext
services.RemoveAll<DbContextOptions<ApplicationDbContext>>();
services.RemoveAll<ApplicationDbContext>();
// Add an in-memory database for testing
services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseInMemoryDatabase("InMemoryDbForTesting");
});
});
}
}Now, in our tests, we use MiApiFactory instead of the generic one.
public class ProductosTests : IClassFixture<MiApiFactory> // 👈 We use ours
{
// ... the test is identical, but now it's safe ...
}Replacing external services
Sometimes we don’t just want to change the database. Imagine your API sends real emails upon registration. You don’t want to send 100 emails every time you run the tests.
We can use the same technique to replace the real IEmailService with a Mock.
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
// ... swap database ...
// Replace IEmailService with a Mock
services.RemoveAll<IEmailService>();
var mockEmail = new Mock<IEmailService>();
services.AddSingleton(mockEmail.Object);
});
}