Overriding Services and Configuration in Tests
Replace databases, gateways, hosted services and settings in the test host with ConfigureTestServices, RemoveAll and test configuration.
factory.WithWebHostBuilder(b =>
{
b.UseSetting("ConnectionStrings:Orders", cs);
b.ConfigureTestServices(s =>
{
s.RemoveAll<IPaymentGateway>();
s.AddSingleton<IPaymentGateway, FakeGateway>();
});
});Integration tests run your real Program.cs, so they also get its real dependencies: the production connection string, the payment gateway client, the background job that polls a queue. The factory gives you hooks to change exactly those pieces and keep everything else real.
Where the hooks run
Subclass WebApplicationFactory<Program> and override ConfigureWebHost, or call factory.WithWebHostBuilder(b => ...) for a one-off variant (a new host each time). Inside, ConfigureTestServices is guaranteed to run after the app's own registrations, which is what lets a replacement win. Plain ConfigureServices also runs after them with WebApplicationBuilder, but ran before them in Startup-based apps, so ConfigureTestServices states the intent and works in both.
Replacing a service correctly
When a service type is registered twice, GetRequiredService<T> returns the last registration, so adding a fake after the app's registration is enough for a single dependency. It is not enough when the code resolves IEnumerable<T> (both run), when the app used TryAdd after you (no effect either way), or for hosted services, which are all started. Use services.RemoveAll<T>() (namespace Microsoft.Extensions.DependencyInjection.Extensions) then add the replacement. To stop a specific BackgroundService, remove the IHostedService descriptor whose ImplementationType is that class.
| Dependency | Replace with |
|---|---|
| Payment, email, SMS gateway | A fake implementation that records calls |
Typed HttpClient | Keep the client, replace the primary handler with a stub |
DbContext | A real database of the same engine (Testcontainers); SQLite only for engine-neutral code |
TimeProvider | FakeTimeProvider registered as a singleton |
| Background poller | Removed; tested on its own |
Replacing the database
Since EF Core 9, AddDbContext stores its configuration lambda as an IDbContextOptionsConfiguration<TContext> service. Removing only DbContextOptions<TContext> and calling AddDbContext again leaves the original UseNpgsql lambda in place, and the context fails with an error about two database providers being registered. Remove both, or better, keep the app's registration and only change its connection string through configuration.
Test configuration
Configuration sources added later win. builder.UseSetting("ConnectionStrings:Orders", value) and an in-memory collection added in ConfigureAppConfiguration override appsettings.json and environment variables. Values that Program.cs reads eagerly from builder.Configuration before Build() are the risky ones; UseSetting is applied early enough for them, and options bound with IOptions<T> are read when first resolved, so they always see test values. Set UseEnvironment("Testing") to stop appsettings.Development.json and user secrets from leaking a developer's local settings into the run.
The failure mode
A suite that overrides too much tests a different application. If the fake repository, fake auth and fake clock all replace production code, a broken DI registration or EF mapping ships green. Override at the process boundary (network, clock, external SaaS) and keep your own code real.
Example
When to use it
- Run the order API's tests with a recording payment gateway so no real card is charged and each test can assert the exact amount charged.
- Remove the outbox dispatcher from the test host so tests do not publish to the production RabbitMQ when a developer's connection string leaks in.
- Point the test host at a per-run PostgreSQL database by overriding ConnectionStrings:Orders with UseSetting instead of editing appsettings.json.
More examples
A reusable factory with the overrides in one place
Tests take <code>IClassFixture<ShopApiFactory></code> and read <code>factory.Payments</code> or move <code>factory.Clock</code>. The fakes are exposed as properties of the factory, so the test and the host share one instance. <code>UseEnvironment("Testing")</code> keeps <code>appsettings.Development.json</code> and user secrets out of the run, and the webhook secret is an obvious placeholder. The fake is thread-safe because tests in other classes may share the factory through a collection fixture.
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Time.Testing;
public sealed class ShopApiFactory : WebApplicationFactory<Program>
{
public FakeTimeProvider Clock { get; } = new(new DateTimeOffset(2026, 3, 2, 9, 0, 0, TimeSpan.Zero));
public RecordingGateway Payments { get; } = new();
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseEnvironment("Testing");
builder.UseSetting("Payments:WebhookSecret", "test-only-secret");
builder.UseSetting("FeatureFlags:SplitShipments", "true");
builder.ConfigureTestServices(services =>
{
services.RemoveAll<TimeProvider>();
services.AddSingleton<TimeProvider>(Clock);
services.RemoveAll<IPaymentGateway>();
services.AddSingleton<IPaymentGateway>(Payments);
// Single, not SingleOrDefault: if the API changes how it registers the dispatcher,
// the suite fails here instead of quietly publishing to the real broker.
var poller = services.Single(d => d.ServiceType == typeof(IHostedService)
&& d.ImplementationType == typeof(OutboxDispatcher));
services.Remove(poller);
});
}
}
public interface IPaymentGateway { Task<string> ChargeAsync(string orderRef, decimal amount, CancellationToken ct); }
public sealed class RecordingGateway : IPaymentGateway
{
private readonly List<(string OrderRef, decimal Amount)> _charges = [];
public IReadOnlyList<(string OrderRef, decimal Amount)> Charges { get { lock (_charges) return [.. _charges]; } }
public Task<string> ChargeAsync(string orderRef, decimal amount, CancellationToken ct)
{
lock (_charges) _charges.Add((orderRef, amount));
return Task.FromResult("ch_test_" + Guid.NewGuid().ToString("N")[..8]);
}
}
public sealed class OutboxDispatcher : BackgroundService
{
protected override Task ExecuteAsync(CancellationToken stoppingToken) => Task.CompletedTask;
}
// Stand-in so this file compiles on its own. Delete it in a real test project: in .NET 10 the API's
// generated Program is already public, and a Program declared here would shadow it (CS0436).
public partial class Program;Swapping the database provider without the two-providers error
Without the <code>IDbContextOptionsConfiguration<OrdersDb></code> removal, EF Core applies both the app's <code>UseNpgsql</code> lambda and the test's <code>UseSqlite</code>, then throws because two providers are registered. SQLite in memory is fast but it is a different engine: no <code>xmin</code> concurrency token, different decimal handling and case rules, and <code>EnsureCreatedAsync</code> skips your migrations. Use it for engine-neutral endpoints and Testcontainers for anything that depends on PostgreSQL behaviour.
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Xunit;
public sealed class SqliteApiFactory : WebApplicationFactory<Program>, IAsyncLifetime
{
// An in-memory SQLite database lives only while one connection to it stays open.
private readonly SqliteConnection _connection = new("Data Source=:memory:");
protected override void ConfigureWebHost(Microsoft.AspNetCore.Hosting.IWebHostBuilder builder) =>
builder.ConfigureTestServices(services =>
{
services.RemoveAll<DbContextOptions<OrdersDb>>();
services.RemoveAll<IDbContextOptionsConfiguration<OrdersDb>>(); // the app's UseNpgsql lambda
services.AddDbContext<OrdersDb>(options => options.UseSqlite(_connection));
});
public async ValueTask InitializeAsync()
{
await _connection.OpenAsync();
using var scope = Services.CreateScope();
await scope.ServiceProvider.GetRequiredService<OrdersDb>().Database.EnsureCreatedAsync();
}
public override async ValueTask DisposeAsync()
{
await base.DisposeAsync();
await _connection.DisposeAsync();
}
}
public sealed class OrdersDb(DbContextOptions<OrdersDb> options) : DbContext(options)
{
public DbSet<OrderRow> Orders => Set<OrderRow>();
}
public sealed class OrderRow
{
public int Id { get; set; }
public required string CustomerId { get; set; }
public decimal Total { get; set; }
}
// Stand-in so this file compiles on its own. Delete it in a real test project: in .NET 10 the API's
// generated Program is already public, and a Program declared here would shadow it (CS0436).
public partial class Program;Settings for every test host
The factory adds the file: <code>builder.ConfigureAppConfiguration((_, c) => c.AddJsonFile(Path.Combine(AppContext.BaseDirectory, "testsettings.json")))</code>. Do not put it in the API project as <code>appsettings.Testing.json</code>: it would ship, and any deployment started with <code>ASPNETCORE_ENVIRONMENT=Testing</code> would fake payments and stop the outbox. Values that <code>Program.cs</code> reads before <code>Build()</code> need <code>UseSetting</code> instead. Raising the rate limit matters: a suite of 400 tests sharing one host and one client IP will otherwise hit a 100-requests-per-minute production limit and fail with 429 at random. Keep secrets out of it; values that differ per test belong in <code>UseSetting</code>.
{
// tests/Shop.Api.IntegrationTests/testsettings.json: lives in the test project
// (copied to output), so it never ships in the API's publish output or image.
"Logging": {
"LogLevel": {
"Default": "Warning",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning"
}
},
"Payments": {
"Mode": "fake",
"WebhookSecret": "<test-placeholder>"
},
"Outbox": {
"Enabled": false
},
"RateLimiting": {
"PermitLimit": 10000
}
}
Discussion