Override services and configuration
Use test-host overrides to replace external boundaries, clocks, configuration values, and other dependencies without changing production startup. Choose factory-wide or scenario-specific scope based on whether the replacement contains mutable test state.
Choose override scope
| Scope | Use it for | Avoid it for |
|---|---|---|
| Factory-wide | Stable adapters, immutable fakes, authentication mappings, common configuration | Per-test expectations, recorded calls, or values mutated by parallel tests |
| Scenario-specific | One test's configuration, service replacement, database connection, or mutable fake | Expensive dependencies intended to be shared across independent factories |
| Early host setting | Values consumed during WebApplication.CreateBuilder or immediately afterward |
Values that can be changed normally through scenario configuration |
Compose factory-wide overrides
With EasyTestHost, register callbacks before Build:
using var factory = EasyTestHost.Create<Program>()
.UseSetting("ConnectionStrings:Orders", connectionString)
.ConfigureConfiguration(configuration =>
configuration.AddInMemoryCollection(testSettings))
.ConfigureServices(services =>
{
services.RemoveAll<IClock>();
services.AddSingleton<IClock>(new FakeClock());
})
.Build();
Callbacks of the same kind execute in registration order. Later service registrations follow the normal Microsoft dependency-injection rules; remove production descriptors before registering a replacement when resolving a single service must be deterministic.
For a derived factory, override ConfigureTestConfiguration,
ConfigureAdditionalServicesForTests, and ConfigureTestAuthentication instead. Keep production
registration in the application and test-only replacement in the test project.
Apply per-scenario overrides
Pass a scope callback when creating the scenario. Configuration and service callbacks apply only to the child host. This executable test proves that replacements and values disappear in the next scope:
[Fact]
public async Task Scope_isolates_database_resources_services_and_configuration()
{
var cancellationToken = TestContext.Current.CancellationToken;
var replacement = new ReplacementCatalogClient();
await using (var first = await _factory.CreateTestScenarioScopeAsync(
scope => scope
.ConfigureConfiguration(configuration =>
configuration.AddInMemoryCollection(new Dictionary<string, string?>
{
["Scenario:Name"] = "first"
}))
.ConfigureServices(services =>
{
services.RemoveAll<IExternalCatalogClient>();
services.AddSingleton<IExternalCatalogClient>(replacement);
}),
cancellationToken))
{
Assert.Same(replacement, first.Services.GetRequiredService<IExternalCatalogClient>());
Assert.Equal("first", first.Services.GetRequiredService<IConfiguration>()["Scenario:Name"]);
await _factory.WithScenarioDbContextAsync(
first,
async (database, token) =>
{
database.Products.Add(new Product { Id = 901, Name = "Scoped", Price = 10m });
await database.SaveChangesAsync(token);
},
cancellationToken);
var productCount = await _factory.WithScenarioDbContextAsync(
first,
(database, token) => database.Products.CountAsync(token),
cancellationToken);
Assert.Equal(1, productCount);
_factory.ExternalCatalog
.When(HttpMethod.Get, "/scope")
.Respond(System.Net.HttpStatusCode.OK);
using var externalClient = new HttpClient(_factory.ExternalCatalog, disposeHandler: false)
{
BaseAddress = new Uri("https://external.example.test/")
};
using var response = await externalClient.GetAsync("/scope", cancellationToken);
_factory.PublishedMessages.Record("test", "scope", new { Value = 42 });
Assert.Equal(1, _factory.ExternalCatalog.CallCount);
Assert.Equal(1, _factory.PublishedMessages.Count);
}
Assert.Equal(0, _factory.ExternalCatalog.CallCount);
Assert.Equal(0, _factory.PublishedMessages.Count);
await using var second = await _factory.CreateTestScenarioScopeAsync(
cancellationToken: cancellationToken);
var secondProductCount = await _factory.WithScenarioDbContextAsync(
second,
(database, token) => database.Products.CountAsync(token),
cancellationToken);
Assert.Equal(0, secondProductCount);
Assert.IsType<ExternalCatalogClient>(
second.Services.GetRequiredService<IExternalCatalogClient>());
Assert.Null(second.Services.GetRequiredService<IConfiguration>()["Scenario:Name"]);
}
When a replacement must be disposed with the child host, create it during scenario configuration
and attach it with TestScenarioContext.DisposeWithScenario.
Override values read during startup
Minimal-host applications can read configuration before ordinary test callbacks run. Use
UseSetting(key, value) or CreateWithHostSettings for connection strings and similar values read
immediately after WebApplication.CreateBuilder.
Use later ConfigureConfiguration callbacks for values resolved through IConfiguration after
the host has started. If changing a value has no effect, identify when production startup reads and
caches it before moving the override earlier.
Diagnose an ineffective replacement
Check these common causes:
- The application resolved or copied the value before the override was applied.
- The production descriptor was not removed, and the consumer resolves a different registration.
- The application uses
IHttpClientFactory, but only a standaloneHttpClientwas replaced. - A singleton captured the original dependency before scenario-specific service configuration.
- The test changed the root factory instead of the isolated scenario child host.
- Options were bound and cached before the test changed their configuration source.
Resolve the service from scope.Services to verify the scenario host sees the intended instance.
Do not resolve scoped services from the root factory provider.