Chapter 36: Testing ASP.NET Core applications
This chapter introduces unit and integration testing strategies and tools for ASP.NET Core-specific components, complete applications, and EF Core-dependent services.
After this chapter you will be able to
- In-memory application integration testing. Explain how the Test Host package and WebApplicationFactory support integration tests by simulating requests to an ASP.NET Core application and verifying its responses.
- Middleware branch behavior. Describe how the sample StatusMiddleware handles requests whose path starts with /ping versus paths that do not match. State that a matching path produces a plain-text pong response, while a nonmatching path passes control to the next RequestDelegate.
- Pass-through delegate test. Explain how to unit test the nonmatching-path case by supplying a DefaultHttpContext and a RequestDelegate that changes a local flag. Describe asserting that the flag changed after invoking the middleware as evidence that the next delegate ran.
- Response body capture. Describe how to test the matching /ping response, including checking its status code, Content-Type, and pong body. Explain that DefaultHttpContext writes to Stream.Null by default, so the test must replace Response.Body with a MemoryStream and read it with a StreamReader after middleware execution.
- Endpoint orchestration responsibilities. Describe the limited responsibilities of controllers, Razor Page handlers, and API endpoints: handle invalid input appropriately, invoke business-logic services for valid requests, return an appropriate result, and optionally perform resource-based authorization. Explain why keeping these components thin makes business logic easier to unit-test.
- Controller unit-test boundaries. Explain that a controller unit test calls an action directly and examines its ActionResult or ActionResult<T>, not the HTTP response sent to the client. Distinguish this isolated test from MVC infrastructure such as routing, model binding, formatting/serialization, and authentication.
- Minimal API unit-test limitations. Explain the limitations of unit-testing minimal API handlers: handlers must be referenceable methods rather than lambdas or local functions, and direct tests exclude endpoint or route-group filters, model binding, and result serialization. Justify favoring thin handlers, unit-tested business services, and integration tests when those framework interactions matter.
- Integration test scope. Explain that this section treats integration tests as tests that exercise multiple components together, rather than isolated tests of one component. Describe that the examples test middleware and an API controller in a context closer to their real use instead of invoking them directly.
- In-memory HTTP testing approach. Describe the section's focus on verifying that an application handles HTTP requests as it would when accessed from a browser, using the ASP.NET Core Microsoft.AspNetCore.TestHost library. State that the section progresses from an in-memory TestServer for middleware to WebApplicationFactory for running a real app configuration, services, and middleware pipeline, including replacement test services for third-party APIs.
- TestServer in-memory integration testing. Explain how the ASP.NET Core Test Host package enables integration-style tests without starting a separate application or listening on a real network port. Describe how TestServer receives in-memory requests while middleware experiences them as normal ASP.NET Core requests.
- Test Host setup sequence. Describe the steps for creating a TestServer integration test: add the Microsoft.AspNetCore.TestHost package, create and configure a HostBuilder, call ConfigureWebHost() to configure the middleware pipeline, call UseTestServer(), start the host with StartAsync(), and obtain a client with GetTestClient().
- WebApplicationFactory integration testing. Explain how WebApplicationFactory runs an in-memory version of a real ASP.NET Core application. Describe that it uses TestServer behind the scenes while retaining the app's configuration, DI registrations, and middleware pipeline.
- WebApplicationFactory test setup. Describe the steps for setting up an xUnit test with WebApplicationFactory<T>: install the testing package, reference a public class from the application, use it as an IClassFixture, inject the factory, and create an HttpClient from it. State why a top-level Program class may need a public partial class declaration.
- Default WebApplicationFactory behavior. Explain that WebApplicationFactory runs the app in memory but otherwise starts it much like dotnet run, using locally available configuration, connection strings, secrets, API keys, and services. State that it uses the Development environment by default, which both verifies that startup works and can unintentionally use local databases or external services.
- Interface-based test substitution. Describe how making CurrencyConverter implement ICurrencyConverter and having the application depend on that interface enables a StubCurrencyConverter to replace the real implementation in tests. Explain that the stub can return a fixed value, such as 3, so the test controls behavior without repeatedly calling the third-party exchange-rate API.
- Per-test DI replacement. Describe using WebApplicationFactory.WithWebHostBuilder to create a customized factory and ConfigureTestServices to replace an already registered service before the in-memory TestServer starts. State that WithWebHostBuilder returns a new factory, leaving the original fixture factory unchanged, and that ConfigureTestServices runs after the app's ConfigureServices.
- Custom factory service configuration. Describe how a custom WebApplicationFactory reduces repeated integration-test setup by overriding ConfigureWebHost and configuring the test services once in the factory.
- Shared and per-test overrides. Distinguish the shared service substitutions in a custom WebApplicationFactory from additional per-test substitutions made with WithWebHostBuilder().
- Database-free test rationale. Explain why unit tests for a service that depends on an EF Core DbContext should avoid a real database, and how an in-memory database supports fast, repeatable, isolated tests.
- In-memory provider comparison. Distinguish the EF Core InMemory provider from SQLite running in in-memory mode, including why the section prefers SQLite for these tests.
- In-memory SQLite test setup. Describe the setup for testing a DbContext-dependent class with SQLite in memory: create and open a SqliteConnection using "DataSource=:memory:", configure DbContextOptions with UseSqlite, call EnsureCreated, seed and save test data, then create a fresh DbContext for the class under test.
What it builds on
- Creating your first test project with xUnit (chapter 35) · Creating an xUnit test project.
- Referencing your app from your test project (chapter 35) · Referencing the application project from a test project.
- Adding Fact and Theory unit tests (chapter 35) · Writing Fact and Theory unit tests.
- Running tests with dotnet test (chapter 35) · Running tests with dotnet test.
- Registering a data context (chapter 12) · Registering and using an EF Core data context.
- Querying data from and saving data to the database (chapter 12) · Querying and saving data with EF Core.
- Building a custom middleware component (chapter 31) · The structure of a custom middleware class with an Invoke method and a next RequestDelegate.
- Creating loosely coupled code (chapter 8) · The separation of loosely coupled services from the components that consume them.
Section by section
Introduction
This chapter introduces unit and integration testing strategies and tools for ASP.NET Core-specific components, complete applications, and EF Core-dependent services.
- ASP.NET Core testing scope: Describe the ASP.NET Core-specific constructs this chapter tests and distinguish isolated unit tests from tests of components working together in the middleware pipeline.
Why it matters: Testing both individual components and their behavior in the pipeline helps reveal different kinds of application problems. - In-memory application integration testing: Explain how the Test Host package and WebApplicationFactory support integration tests by simulating requests to an ASP.NET Core application and verifying its responses.
Why it matters: These tools let you verify real application behavior without needing to run the application as an externally hosted server. - In-memory EF Core database testing: Describe using the SQLite EF Core provider with an in-memory database to test services that depend on a DbContext without relying on a real database.
Why it matters: An in-memory database avoids dependence on unknown shared database infrastructure and the need to reset a database between tests.
Key terms: unit tests, integration tests, custom middleware, API controllers, minimal API endpoints, middleware pipeline, Test Host package, WebApplicationFactory, SQLite database provider, Entity Framework Core
36.1 Unit testing custom middleware
This section shows how to unit test a custom middleware component in isolation for both pass-through and response-producing requests.
- Middleware branch behavior: Describe how the sample StatusMiddleware handles requests whose path starts with /ping versus paths that do not match. State that a matching path produces a plain-text pong response, while a nonmatching path passes control to the next RequestDelegate.
Why it matters: Testing both branches verifies that middleware responds when it should and does not block the rest of the pipeline when it should not. - Pass-through delegate test: Explain how to unit test the nonmatching-path case by supplying a DefaultHttpContext and a RequestDelegate that changes a local flag. Describe asserting that the flag changed after invoking the middleware as evidence that the next delegate ran.
Why it matters: This gives you a simple way to prove isolated middleware passes requests onward rather than silently stopping them. - Response body capture: Describe how to test the matching /ping response, including checking its status code, Content-Type, and pong body. Explain that DefaultHttpContext writes to Stream.Null by default, so the test must replace Response.Body with a MemoryStream and read it with a StreamReader after middleware execution.
Why it matters: Capturing the response stream lets a unit test verify the actual content middleware writes, not just that it was invoked. - Middleware testing trade-offs: Explain that isolated middleware tests require substantial HttpContext setup and can become tightly coupled to implementation details, even though they test the component independently. State that integration testing may be easier for simple middleware with no database or service dependencies.
Why it matters: Knowing this trade-off helps you choose whether an isolated unit test or an in-memory integration test is the clearer test.
Key terms: StatusMiddleware, Invoke, RequestDelegate, /ping, pong, DefaultHttpContext, HttpContext, Assert, Response.Body, Stream.Null
36.2 Unit testing API controllers and minimal API endpoints
This section evaluates unit testing controllers and minimal API endpoints in isolation, showing how to do it and why thin endpoints plus integration tests are often preferable.
- Endpoint orchestration responsibilities: Describe the limited responsibilities of controllers, Razor Page handlers, and API endpoints: handle invalid input appropriately, invoke business-logic services for valid requests, return an appropriate result, and optionally perform resource-based authorization. Explain why keeping these components thin makes business logic easier to unit-test.
Why it matters: Thin endpoint classes separate HTTP orchestration from business behavior, making the valuable logic easier to test and change. - Controller unit-test boundaries: Explain that a controller unit test calls an action directly and examines its ActionResult or ActionResult<T>, not the HTTP response sent to the client. Distinguish this isolated test from MVC infrastructure such as routing, model binding, formatting/serialization, and authentication.
Why it matters: Knowing the test boundary prevents you from assuming that a controller unit test verifies framework behavior it never runs. - Manual ModelState setup: Describe how to unit-test a controller's invalid-model branch by manually adding a model-binding error to ModelState before invoking the action, then asserting the expected bad-request result. Explain that the model should be kept consistent with ModelState even though no model binding runs in the unit test.
Why it matters: Manually setting ModelState lets an isolated test reach validation-error logic that the running MVC framework would normally set up. - Minimal API unit-test limitations: Explain the limitations of unit-testing minimal API handlers: handlers must be referenceable methods rather than lambdas or local functions, and direct tests exclude endpoint or route-group filters, model binding, and result serialization. Justify favoring thin handlers, unit-tested business services, and integration tests when those framework interactions matter.
Why it matters: This helps you choose tests that cover realistic endpoint behavior without overinvesting in fragile tests of minimal orchestration code.
Key terms: controllers, Razor Pages, PageModels, business logic services, resource-based authorization, orchestrators, unit test, action method, ActionResult<T>, MVC formatter infrastructure
36.3 Integration testing: Testing your whole app in-memory
This introduction defines integration testing in contrast to unit testing and outlines testing ASP.NET Core HTTP request handling in memory with TestServer and WebApplicationFactory.
- Integration test scope: Explain that this section treats integration tests as tests that exercise multiple components together, rather than isolated tests of one component. Describe that the examples test middleware and an API controller in a context closer to their real use instead of invoking them directly.
Why it matters: Integration tests reveal whether application components work together correctly in the request-handling environment. - Unit and integration test balance: Describe why integration tests complement rather than replace unit tests: unit tests are well suited to small component logic and are typically quick, whereas integration tests need more configuration, can depend on external infrastructure, and are normally slower. State that an application commonly has many more unit tests and only a smaller number of integration tests after extensive unit-test coverage.
Why it matters: Choosing the right balance keeps test suites both thorough and practical to run. - In-memory HTTP testing approach: Describe the section's focus on verifying that an application handles HTTP requests as it would when accessed from a browser, using the ASP.NET Core Microsoft.AspNetCore.TestHost library. State that the section progresses from an in-memory TestServer for middleware to WebApplicationFactory for running a real app configuration, services, and middleware pipeline, including replacement test services for third-party APIs.
Why it matters: In-memory HTTP testing lets you verify realistic request handling without requiring a separately running web server.
Key terms: integration tests, unit tests, StatusMiddleware, API controller, external infrastructure, Microsoft.AspNetCore.TestHost, TestServer, WebApplicationFactory, middleware pipeline, third-party APIs
36.3.1 Creating a TestServer using the Test Host package
This section shows how to create an in-memory integration test host with the Test Host package and send requests to it through an HttpClient.
- TestServer in-memory integration testing: Explain how the ASP.NET Core Test Host package enables integration-style tests without starting a separate application or listening on a real network port. Describe how TestServer receives in-memory requests while middleware experiences them as normal ASP.NET Core requests.
Why it matters: This lets you test middleware with real ASP.NET Core hosting infrastructure while avoiding fragile process, port, startup, and shutdown concerns. - Test Host setup sequence: Describe the steps for creating a TestServer integration test: add the Microsoft.AspNetCore.TestHost package, create and configure a HostBuilder, call ConfigureWebHost() to configure the middleware pipeline, call UseTestServer(), start the host with StartAsync(), and obtain a client with GetTestClient().
Why it matters: These steps create a focused test application that can make realistic in-memory requests to a configured middleware pipeline. - Focused middleware test scope: Distinguish a manually configured TestServer test for one middleware component from an integration test of the real application. Explain why the focused HostBuilder setup is useful for custom middleware but does not automatically represent the application's complete production configuration.
Why it matters: Choosing the right test scope keeps middleware tests simple while recognizing when a test must use the real application's configuration.
Key terms: Test Host, Microsoft.AspNetCore.TestHost, TestServer, in-memory requests, Kestrel, IHost, HostBuilder, ConfigureWebHost, Configure, UseTestServer
36.3.2 Testing your application with WebApplicationFactory
This section shows how to use WebApplicationFactory to run and test a real ASP.NET Core application in memory with its normal configuration, services, and middleware pipeline.
- WebApplicationFactory integration testing: Explain how WebApplicationFactory runs an in-memory version of a real ASP.NET Core application. Describe that it uses TestServer behind the scenes while retaining the app's configuration, DI registrations, and middleware pipeline.
Why it matters: This lets you test the behavior users see without manually rebuilding your application's hosting setup in every test. - WebApplicationFactory test setup: Describe the steps for setting up an xUnit test with WebApplicationFactory<T>: install the testing package, reference a public class from the application, use it as an IClassFixture, inject the factory, and create an HttpClient from it. State why a top-level Program class may need a public partial class declaration.
Why it matters: Correct fixture setup gives tests an HttpClient that can send in-memory requests through the real application. - Behavior-focused integration tests: Distinguish a WebApplicationFactory test of an application's response to an input from a test of a specific internal component. Explain that behavior-focused tests avoid coupling to implementation details and are less likely to become brittle when the implementation changes.
Why it matters: Tests that assert externally visible behavior can remain useful even when you refactor the internals of your application. - Real dependency side effects: State that WebApplicationFactory runs the application realistically enough for real dependencies to perform side effects, such as writing to a database or calling third-party APIs. Explain why dependencies may need to be replaced for safe and manageable tests.
Why it matters: Recognizing real side effects prevents integration tests from accidentally changing production-like data or contacting external systems.
Key terms: WebApplicationFactory, TestServer, Microsoft.AspNetCore.Mvc.Testing, dependency injection, middleware pipeline, WebApplicationFactory<T>, IClassFixture<TFixture>, Program, public partial class Program, HttpClient
36.3.3 Replacing dependencies in WebApplicationFactory
This section shows how to replace an application's real dependencies with test doubles when using WebApplicationFactory for integration tests.
- Default WebApplicationFactory behavior: Explain that WebApplicationFactory runs the app in memory but otherwise starts it much like dotnet run, using locally available configuration, connection strings, secrets, API keys, and services. State that it uses the Development environment by default, which both verifies that startup works and can unintentionally use local databases or external services.
Why it matters: Knowing what the test host inherits prevents integration tests from accidentally contacting real local or third-party resources. - Interface-based test substitution: Describe how making CurrencyConverter implement ICurrencyConverter and having the application depend on that interface enables a StubCurrencyConverter to replace the real implementation in tests. Explain that the stub can return a fixed value, such as 3, so the test controls behavior without repeatedly calling the third-party exchange-rate API.
Why it matters: Depending on an interface rather than a concrete service makes external behavior controllable and keeps integration tests independent of live APIs. - Per-test DI replacement: Describe using WebApplicationFactory.WithWebHostBuilder to create a customized factory and ConfigureTestServices to replace an already registered service before the in-memory TestServer starts. State that WithWebHostBuilder returns a new factory, leaving the original fixture factory unchanged, and that ConfigureTestServices runs after the app's ConfigureServices.
Why it matters: This lets a single integration test safely override a real dependency without adding test-only behavior to the production application.
Key terms: WebApplicationFactory, Development, in-memory, dotnet run, CurrencyConverter, ICurrencyConverter, StubCurrencyConverter, IHttpClientFactory, testable, WithWebHostBuilder
36.3.4 Reducing duplication by creating a custom WebApplicationFactory
This section shows how a custom WebApplicationFactory centralizes recurring integration-test service replacements while preserving optional per-test overrides.
- Custom factory service configuration: Describe how a custom WebApplicationFactory reduces repeated integration-test setup by overriding ConfigureWebHost and configuring the test services once in the factory.
Why it matters: Centralizing services that every test must replace keeps integration tests shorter and avoids duplicated setup. - Custom factory fixture usage: Explain that tests use a custom WebApplicationFactory by injecting it as an IClassFixture, as with the regular factory, so the configured factory is available to the test class.
Why it matters: Using the factory as a class fixture lets tests access the shared, preconfigured in-memory application host. - Shared and per-test overrides: Distinguish the shared service substitutions in a custom WebApplicationFactory from additional per-test substitutions made with WithWebHostBuilder().
Why it matters: Combining the two approaches provides both low-duplication defaults and control for tests with special dependency needs.
Key terms: WebApplicationFactory, ConfigureWebHost, test services, IClassFixture, WithWebHostBuilder, per-test configuration
36.4 Isolating the database with an in-memory EF Core provider
This section shows how to test EF Core-dependent code quickly and in isolation by using an in-memory SQLite database instead of a real database.
- Database-free test rationale: Explain why unit tests for a service that depends on an EF Core DbContext should avoid a real database, and how an in-memory database supports fast, repeatable, isolated tests.
Why it matters: Keeping database dependencies out of unit tests makes test results faster and less dependent on external configuration. - In-memory provider comparison: Distinguish the EF Core InMemory provider from SQLite running in in-memory mode, including why the section prefers SQLite for these tests.
Why it matters: Choosing SQLite in memory gives tests relational database behavior that the EF Core InMemory provider does not simulate. - In-memory SQLite test setup: Describe the setup for testing a DbContext-dependent class with SQLite in memory: create and open a SqliteConnection using "DataSource=:memory:", configure DbContextOptions with UseSqlite, call EnsureCreated, seed and save test data, then create a fresh DbContext for the class under test.
Why it matters: This setup creates a fast test database whose data can be persisted and queried just as the service under test expects. - Connection lifetime and context separation: Explain that an in-memory SQLite database is destroyed when its connection closes, so the test must explicitly keep the connection open to share the database across DbContexts; also explain why separate contexts are used for seeding and testing.
Why it matters: Managing the connection and contexts correctly prevents a vanished test database and avoids false results caused by EF Core's change tracking cache.
Key terms: DbContext, unit tests, in-memory database provider, isolated tests, Microsoft.EntityFrameworkCore.InMemory, Microsoft.EntityFrameworkCore.Sqlite, SQLite, relational database, in-memory mode, constraints
36.5 Summary
This summary recaps unit and integration testing techniques for ASP.NET Core components, whole applications, and EF Core database code.
Concepts explained in this chapter
- integration testAn integration test evaluates the interaction of multiple application components or a substantial vertical slice of an application.
- testA test is an automated program method that evaluates whether application behavior matches an expected result.
- WebApplicationFactoryWebApplicationFactory is an ASP.NET Core testing API that bootstraps an application for in-memory functional and integration testing and permits test-specific customization.
- Microsoft.EntityFrameworkCore.SQLiteMicrosoft.EntityFrameworkCore.SQLite is the NuGet package that supplies SQLite database-provider integration for Entity Framework Core.
- in-memory databaseAn in-memory database provider is a database-provider implementation that stores test data in process memory instead of persistent database storage.
- Microsoft.AspNetCore.TestHostMicrosoft.AspNetCore.TestHost is an ASP.NET Core library that provides infrastructure for hosting and testing applications in memory.
- TestServerTestServer is an ASP.NET Core in-memory server that accepts test requests and returns responses without requiring a network server.
- EnsureCreatedEnsureCreated is an Entity Framework Core database method that creates the database and its schema if they do not already exist, without applying migrations.
- SqliteConnectionSqliteConnection is the Microsoft.Data.Sqlite connection type used to connect an application to a SQLite database.
- UseSqlite()UseSqlite() is an Entity Framework Core configuration method that selects SQLite as the database provider for a context.
- IClassFixture<TFixture>IClassFixture<TFixture> is an xUnit interface that provides one shared fixture instance to the tests in a class.
- Microsoft.AspNetCore.Mvc.TestingMicrosoft.AspNetCore.Mvc.Testing is a .NET package that provides infrastructure for hosting and testing ASP.NET Core applications in memory.
- WithWebHostBuilderWithWebHostBuilder is a WebApplicationFactory method that returns a customized test factory configured through an application host builder.
- DefaultHttpContextDefaultHttpContext is an ASP.NET Core implementation of HttpContext that provides default request and response state for an HTTP request.
- memory streamMemoryStream is a .NET stream implementation that stores readable and writable data in memory.
- ModelStateModelState is the ASP.NET Core request-state dictionary that stores bound values and validation errors associated with properties or the model.
- business logicBusiness logic is the application behavior that implements domain rules, decisions, and processes independently of presentation or infrastructure concerns.
- minimal APIA minimal API endpoint is an addressable operation in a minimal API application that handles a request with minimal surrounding framework structure.
- result resourceActionResult<T> is an ASP.NET Core MVC result type that allows an action method to return either a typed value or an arbitrary HTTP action result.
- middlewareMiddleware is a request-processing component that can inspect, modify, or handle HTTP requests and responses within an ASP.NET Core pipeline.
- brittle testA brittle test is a test that fails when an implementation changes even though the externally observable behavior remains correct.
- GetTestClientGetTestClient is a TestServer extension method that creates an HttpClient configured to send requests to the server in memory.
- HostBuilderHostBuilder is a .NET hosting API type that configures application services, hosting, configuration, and middleware before creating an application host.
- UseTestServerUseTestServer is an ASP.NET Core hosting extension method that configures an application to use TestServer instead of a network web server.
- HttpContextHttpContext is the ASP.NET Core object that represents the information and state associated with a single HTTP request and its response.
- RequestDelegateRequestDelegate is an ASP.NET Core delegate type representing a function that processes an HTTP context and returns asynchronously.
- contractA web API is an HTTP-accessible application interface that exposes operations or data to clients.
- controllerA web API controller is an ASP.NET Core controller that handles HTTP API requests through action methods and produces machine-readable responses.
- DbContextOptionsDbContextOptions is the Entity Framework Core configuration object containing the settings and services used by a database context.
- DbContextOptionsBuilderDbContextOptionsBuilder is an Entity Framework Core builder type for configuring the options and services used by a database context.
- SaveChangesSaveChanges is an Entity Framework Core database-context method that persists tracked data changes to the configured database.
- StartAsyncStartAsync is an asynchronous host method that starts a configured application and returns when startup processing has completed.
These guides are generated from the text of Manning books by Manning's knowledge graph and link to the sections they draw on. The full books are available on liveBook and at manning.com.
