HT
How Things Work
System Online

Integration Testing (ASP.NET)

Verifying components work together through the real framework — WebApplicationFactory, test servers, swapping in test databases, and the isolation that keeps it all reliable.

How It Works

Integration tests verify that components cooperate correctly through the real framework — routing, middleware, serialization, business logic, and a real (test) database — rather than in isolation. In ASP.NET Core, WebApplicationFactory boots the app in-process so tests send genuine HTTP requests. They're slower than unit tests but catch the wiring and persistence bugs that mocked unit tests structurally cannot.

1
Test Components Together

An integration test verifies that multiple parts of the system work correctly when wired together — controller, middleware, validation, business logic, and the database — through the real framework. Unit tests prove a piece works in isolation; integration tests prove the pieces connect.

2
WebApplicationFactory

In ASP.NET Core, WebApplicationFactory boots your actual application in-process, with its real DI container, middleware pipeline, and routing. You get an HttpClient that sends genuine HTTP requests to your endpoints — exercising serialization, model binding, filters, and the full request path that unit tests skip.

3
Real Pipeline, Test Dependencies

You keep the framework real but substitute volatile externals: swap the production database for an in-memory provider or (for higher fidelity) a real database in a throwaway container via Testcontainers. Seed known fixtures, run the request, then assert both the HTTP response and the resulting database state.

4
Where They Sit & Their Cost

Integration tests are the middle of the test pyramid — fewer than unit tests, more than E2E. They're slower (a server and DB spin up) and need careful isolation (a clean DB per test/class) to stay reliable, but they catch the configuration, serialization, and SQL bugs that mocked unit tests never see.

Key Concepts

🔗Integration Test

Verifies multiple components working together through the real framework, not in isolation.

🏭WebApplicationFactory

ASP.NET Core's in-process test host that boots the real app and gives you an HttpClient.

🖥️Test Server

An in-memory server running the real pipeline so tests send genuine HTTP requests without a network.

📦Testcontainers

Spins up real dependencies (Postgres, Redis) in disposable Docker containers for high-fidelity tests.

🔧Service Override

Replacing specific registered services (e.g. the DB) at test time while keeping the rest real.

🧼Test Isolation

A clean, independent database/state per test so tests don't leak into and break each other.

WebApplicationFactory integration test
tsx
1// ASP.NET Core integration test — boots the REAL app in-process.
2public class OrdersApiTests : IClassFixture<WebApplicationFactory<Program>> {
3 private readonly HttpClient _client;
4
5 public OrdersApiTests(WebApplicationFactory<Program> factory) {
6 _client = factory.WithWebHostBuilder(b => {
7 b.ConfigureServices(services => {
8 // swap the real DB for a test database
9 services.RemoveAll<DbContextOptions<AppDb>>();
10 services.AddDbContext<AppDb>(o => o.UseInMemoryDatabase("test"));
11 // (better: Testcontainers with real Postgres for fidelity)
12 });
13 }).CreateClient();
14 }
15
16 [Fact]
17 public async Task Post_Order_Persists_And_Returns_201() {
18 // Act — a REAL HTTP request through routing, middleware, the controller
19 var res = await _client.PostAsJsonAsync("/api/orders", new { items = new[] { 1 } });
20
21 // Assert — the response AND the persisted state
22 res.StatusCode.Should().Be(HttpStatusCode.Created);
23 var saved = await res.Content.ReadFromJsonAsync<OrderDto>();
24 saved!.Id.Should().NotBeEmpty();
25 }
26}
💡
Why This Matters

Integration tests close the confidence gap that unit tests leave — they prove your endpoints, configuration, and database access actually work together. Knowing how to write them (test hosts, service overrides, isolation, Testcontainers) is essential for backend reliability and is a common topic for backend and platform interviews.

Common Pitfalls

Shared database state across tests causing flaky, order-dependent failures — isolate per test.
Over-using slow integration tests for logic that a fast unit test should cover (an inverted pyramid).
In-memory DB providers that don't behave like the real database, giving false confidence — prefer Testcontainers for fidelity.
Mocking so much that the test no longer exercises real integration — defeating the point.
Not cleaning up resources (containers, connections), making the suite slow and leaky in CI.
Real-World Use Cases

1Green Unit Tests, Broken Endpoint

Scenario

Every unit test passes, but in production a new endpoint returns 500. The cause: a misconfigured JSON serializer setting and a missing DI registration — things no unit test touched because they all mocked the framework.

Problem

Unit tests deliberately bypass the framework (routing, model binding, serialization, DI wiring), so whole categories of bugs live in the gaps between components and never get exercised until production.

Solution

Add integration tests with WebApplicationFactory that send real HTTP requests through the actual pipeline against a test database. The misconfigured serializer and missing registration fail the integration test immediately, before deploy — exactly the wiring unit tests can't cover.

💡

Takeaway: Unit tests verify logic; integration tests verify wiring. The bugs that hide between components (serialization, DI, routing, SQL) only surface when you test through the real framework end-to-end.

2Flaky Tests From Shared Database State

Scenario

Integration tests pass when run alone but fail randomly in CI. Tests run in parallel and share one database, so one test's data leaks into another's assertions, producing non-deterministic failures.

Problem

Without isolation, integration tests interfere through shared state. Order-dependent and parallel-execution flakiness erodes trust in the suite, and developers start ignoring red builds.

Solution

Isolate state per test: give each test class its own database (unique in-memory name or a fresh container), reset/seed within a transaction that rolls back, or use respawn to wipe between tests. Tests become deterministic regardless of order or parallelism.

💡

Takeaway: Integration tests are only valuable if they're reliable. Enforce strict state isolation — a clean database per test — so parallel, order-independent runs stay deterministic and trustworthy.

Join the discussion

Comments

?

Loading comments...

Cookie preferences

We use essential cookies for sign-in and preferences. With your permission, we also use basic analytics to improve lessons.