End‑to‑end (a.k.a. full stack) integration testing example for two communicating ASP.NET Core APIs hosted entirely in the test process.
This repository demonstrates how to:
- Spin up multiple ASP.NET Core services side‑by‑side inside an xUnit test using WebApplicationFactory.
- Exercise real HTTP calls between those services (no mocks, no in‑memory shortcuts) without deploying anything externally.
- Replace a named HttpClient (configured in the application under test) with a test-provided HttpClient that targets another in‑process service instance.
- Override IHttpClientFactory in a controlled way so the production registration (AddHttpClient / typed clients) stays untouched.
There are two services:
- WebService (utility / downstream service) – exposes
GET /weatherForecast
returning 5 forecast items. - WebApplication (master / upstream service) – exposes
GET /weatherForecast
which internally calls the downstream service twice and concatenates the results (expecting 10 items total).
In production, WebApplication configures a named HttpClient "WebService" with its base address coming from configuration key ServerUrl
.
During tests we do not want to hit a real deployed WebService. Instead we host an in‑process copy of WebService and transparently redirect WebApplication's outgoing HttpClient calls to it.
Two factory instances are created inside the test: one for WebApplication and one for WebService. Each yields an HttpClient pointed at the respective in‑memory server.
CustomWebApplicationFactory
overrides the DI registration for IHttpClientFactory
with a CustomHttpClientFactory
that serves pre-created named HttpClient instances from a concurrent dictionary.
// In test
var webAppFactory = new CustomWebApplicationFactory();
var webAppClient = webAppFactory.CreateClient();
var webServiceFactory = new WebApplicationFactory<WebService.Controllers.WeatherForecastController>();
var webServiceClient = webServiceFactory.CreateClient();
// Provide the downstream client under the name expected by the app
webAppFactory.AddHttpClient("WebService", webServiceClient);
Because we replace the entire IHttpClientFactory
, the typed client (WeatherForecastClient
) receives our injected downstream HttpClient instead of one built from production configuration (ServerUrl
). No code changes to the application are required.
WeatherForecastClient
is registered as a typed client for the named client "WebService" and simply performs two GET calls, concatenating responses.
The downstream service returns 5 items per call; the upstream endpoint must therefore yield 10. The test asserts both HTTP success and item count.
- Start WebApplication (under test).
- Start WebService (dependency) in the same process space via its own factory.
- Register the WebService HttpClient instance under the expected name ("WebService").
- Issue a GET to
/weatherForecast
on WebApplication. - WebApplication internally performs two real HTTP calls to WebService.
- Response is validated (HTTP 200 + 10 forecast items).
Alternative approaches (e.g., editing configuration, using DelegatingHandlers, or mocking the typed client) either:
- Couple the test to implementation details (base addresses, configuration keys), or
- Bypass real HTTP behavior (losing coverage of serialization, middleware, etc.).
Replacing IHttpClientFactory
lets the original AddHttpClient
& typed client registration stay intact, while seamlessly redirecting network calls inside the same process with minimal plumbing.
Prerequisites: .NET 9 SDK.
Build:
dotnet build
Run tests:
dotnet test
Optional: run services individually (in two terminals) if you want to browse Swagger UIs.
cd src/WebService && dotnet run
cd src/WebApplication && dotnet run
Then open the Swagger endpoints displayed in the console output.
ServerUrl
inappsettings.json
is only used in production hosting; tests bypass it by replacing the factory.- The approach keeps tests black‑box from the perspective of HTTP boundaries while remaining fully in‑memory (no real sockets bound externally).
- Suitable for verifying serialization, filters, middleware, routing, DI wiring, and inter‑service orchestration logic together.