Testing
|
This section documents ASP.NET Core on .NET 10 (LTS), the current release — the minimal hosting model, the middleware pipeline, dependency injection, Minimal APIs, MVC & Razor Pages, Blazor with the current render modes, SignalR and gRPC, EF Core, ASP.NET Core Identity and policy-based authorization, output caching, rate limiting, and Native-AOT-aware building — as described by the official documentation at Microsoft Learn, which is the reference these pages are written and verified against. This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. .NET ships a major release every November and its APIs continue to evolve: the examples here target .NET 10 / C# 14. This section’s bibliography lists the reference material consulted while preparing these pages. |
ASP.NET Core is designed for testing: DI makes dependencies replaceable, and WebApplicationFactory runs the
whole app in memory. See Testing in ASP.NET Core.
The test project
dotnet new xunit -o Shop.Tests
dotnet add Shop.Tests reference Shop.Web
dotnet add Shop.Tests package Microsoft.AspNetCore.Mvc.Testing
dotnet add Shop.Tests package NSubstitute
public sealed class PriceCalculatorTests
{
[Theory]
[InlineData(100, 0.2, 120)]
[InlineData(0, 0.2, 0)]
public void Applies_vat(decimal net, decimal rate, decimal expected)
{
var sut = new PriceCalculator();
var gross = sut.WithVat(net, rate); // Arrange / Act
Assert.Equal(expected, gross); // Assert
}
}
Use Moq or NSubstitute for test doubles of interfaces you inject.
Unit tests
Instantiate the class under test directly and pass fakes:
[Fact]
public async Task Get_returns_404_when_missing()
{
var svc = Substitute.For<IOrderService>();
svc.FindAsync(7).Returns((Order?)null);
var controller = new OrdersController(svc);
var result = await controller.Get(7);
Assert.IsType<NotFoundResult>(result.Result);
}
// custom middleware: feed it a DefaultHttpContext
[Fact]
public async Task RequestId_middleware_sets_header()
{
var ctx = new DefaultHttpContext();
var mw = new RequestIdMiddleware(_ => Task.CompletedTask);
await mw.InvokeAsync(ctx, NullLogger<RequestIdMiddleware>.Instance);
Assert.True(ctx.Response.Headers.ContainsKey("X-Request-Id"));
}
The same approach covers Razor Page models, minimal-endpoint handler methods (extract them as named methods),
validators, and AuthorizationHandler classes. See
Unit test controller logic.
Integration tests
WebApplicationFactory<TEntryPoint> boots the real pipeline in memory and hands you an HttpClient:
public sealed class OrdersApiTests(WebApplicationFactory<Program> factory)
: IClassFixture<WebApplicationFactory<Program>>
{
[Fact]
public async Task Post_then_get_roundtrips()
{
var client = factory.WithWebHostBuilder(b => b.ConfigureTestServices(services =>
{
services.RemoveAll<IPaymentGateway>();
services.AddSingleton<IPaymentGateway, FakePaymentGateway>();
})).CreateClient();
var created = await client.PostAsJsonAsync("/orders", new { customerId = 1 });
created.EnsureSuccessStatusCode();
var url = created.Headers.Location!;
var order = await client.GetFromJsonAsync<OrderDto>(url);
Assert.Equal(1, order!.CustomerId);
}
}
Requires Program to be reachable (top-level statements expose an implicit public partial class Program).
TestServer is the lower-level primitive underneath. See
Integration tests in ASP.NET Core.
Test infrastructure
-
Config overrides:
builder.UseSetting("ConnectionStrings:Default", …)or an in-memory config source. -
Test auth: register an
AuthenticationHandler<AuthenticationSchemeOptions>that always returns a knownClaimsPrincipal, so[Authorize]endpoints are reachable without a real identity provider. -
Database: point EF Core at SQLite in-memory (a kept-open connection) for a real relational store per test; seed it in the fixture.
-
Antiforgery: parse the token from the returned HTML with AngleSharp and echo it on the POST.
-
Assertions: deserialize
ProblemDetailsand assert onStatus/Title/Errors.
public sealed class TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> o, ILoggerFactory l, UrlEncoder e)
: AuthenticationHandler<AuthenticationSchemeOptions>(o, l, e)
{
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new[] { new Claim(ClaimTypes.Name, "test"), new Claim(ClaimTypes.Role, "Admin") };
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(new ClaimsIdentity(claims, "Test")), "Test");
return Task.FromResult(AuthenticateResult.Success(ticket));
}
}