The host defines scope
WebApplicationFactory prepares a test host and a client that can traverse routing, middleware and configured services. This differs from directly calling an endpoint function. However, TestServer does not establish the load-balancer path, served certificate or production infrastructure behavior.
Before interpreting a result, identify which components are in the host, which were replaced and which never received the request.
First response and final response
A client following redirects can receive login HTML with 200 after an initial 302. If the objective is to observe the challenge of a Razor page with cookies and configured redirection, use AllowAutoRedirect=false and inspect status and Location.
Do not generalize this scenario to a JWT bearer API, whose challenge is 401. An HTTPS BaseAddress also does not turn TestServer into evidence of deployed TLS.
Replace a dependency
ConfigureTestServices permits a controlled implementation for the scenario. In the excerpt, RemoveAll removes IStockLookup registrations and the stub returns null; the assertion observes that result becoming 404. Real database integration is outside this request.
Establish that the endpoint actually uses the interface and that no other implementation was captured earlier. Document replacement alongside the conclusion to avoid attributing an omitted query to the test.
Test identity
A test handler can create a principal with synthetic claims through AuthenticateResult.Success. Its scheme must match the scheme selected by the host. This technique supports studying policy decisions for known identities, including present or absent permissions.
The signature, issuer, audience and lifetime of a real JWT are not validated by that fabricated identity. Keep authorization claims separate from claims about credential acceptance.
Initial state and effects
Each scenario should establish the data it depends on. If one test deletes rows used by another, an order-dependent failure can result from preparation without establishing an endpoint defect. Isolate stores or restore data in a controlled manner.
When the contract says reject without creating, observe both the validation response and subsequent state. A 400 alone does not prove no write occurred; absence of an exception in the test method does not confirm the functional contract either.
Provider and real query
SQLite can be useful as a relational substitute, but text comparisons, LINQ translation and provider-specific SQL can differ from production SQL Server. EF InMemory is not a relational database and does not demonstrate SQL transactional rollback.
A repository returning ready-made DTOs also removes the query from the exercised boundary. Select verification appropriate to the claim: consumer response, query behavior or provider semantics. No substitute automatically proves all three.
Conclusion bounded by configuration
UseEnvironment("Development") makes a test environment explicit but does not automatically check configuration and error-handling branches used in Production. The same applies to replaced authentication, synthetic state and omitted external services.
In an acceptance record, describe the request, expected result, observed state and excluded boundaries. If a fixed two-DTO list produced 200, the conclusion covers the consumer; claiming correct SQL filtering requires evidence from the relevant implementation and provider.
// Reading example for an ASP.NET Core 10 xUnit test project.
// Prerequisites: Microsoft.AspNetCore.Mvc.Testing 10.x, xUnit,
// a reference to the SUT, and its public Program and IStockLookup types.
// SUT contract: anonymous GET /stock/{id:int} calls Find(id);
// null produces 404, otherwise 200 with JSON { id, units }.
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Xunit;
public sealed class StockResponseTests
{
[Fact]
public async Task Missing_stock_returns_not_found()
{
using var baseFactory = new WebApplicationFactory<Program>();
using var factory = baseFactory.WithWebHostBuilder(builder =>
builder.ConfigureTestServices(services =>
{
services.RemoveAll<IStockLookup>();
services.AddSingleton<IStockLookup>(new MissingStock());
}));
using var client = factory.CreateClient(
new WebApplicationFactoryClientOptions { AllowAutoRedirect = false });
using var response = await client.GetAsync("/stock/42");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
// SUT interface premise: StockRow? Find(int id).
// StockRow and IStockLookup are supplied by the referenced SUT.
private sealed class MissingStock : IStockLookup
{
public StockRow? Find(int id) => null;
}
}The example is a reading excerpt with declared SUT contracts, not a standalone project. The request traverses a host with IStockLookup replaced; the assertion checks 404 for a null result. It does not check the real query, authentication, writes or production TLS.
If the SUT implementation has another contract or uses another dependency, adapt the excerpt. Assessment asks learners to interpret these boundaries without executing the test.
Common pitfalls
Following a redirect to login can turn an initial challenge into a final 200. Synthetic claims do not validate JWT. SQLite and InMemory do not reproduce every production-provider rule. Shared state without reset makes results order-dependent.
Related topics: Authentication and authorization · Persistence and concurrency · Health, observability and diagnosis
Name the observed behavior and dependencies actually traversed. Keep each scenario’s premises explicit and choose additional evidence for excluded boundaries.
Reference: Integration host and request path · ASP.NET Core 10.0 / .NET 10 / C# 14