Integration tests ensure that an app's components function correctly at a level that includes the app's supporting infrastructure, such as the database, file system, and network. ASP.NET Core supports integration tests using a unit test framework with a test web host and an in-memory test server.
This article assumes a basic understanding of unit tests. If unfamiliar with test concepts, see the Unit Testing in .NET Core and .NET Standard article and its linked content.
The sample app is a Razor Pages app and assumes a basic understanding of Razor Pages. If you're unfamiliar with Razor Pages, see the following articles:
For testing SPAs, we recommend a tool such as Playwright for .NET, which can automate a browser.
Introduction to integration tests
Integration tests evaluate an app's components on a broader level than unit tests. Unit tests are used to test isolated software components, such as individual class methods. Integration tests confirm that two or more app components work together to produce an expected result, possibly including every component required to fully process a request.
These broader tests are used to test the app's infrastructure and whole framework, often including the following components:
Database
File system
Network appliances
Request-response pipeline
Unit tests use fabricated components, known as fakes or mock objects, in place of infrastructure components.
In contrast to unit tests, integration tests:
Use the actual components that the app uses in production.
Require more code and data processing.
Take longer to run.
Therefore, limit the use of integration tests to the most important infrastructure scenarios. If a behavior can be tested using either a unit test or an integration test, choose the unit test.
In discussions of integration tests, the tested project is frequently called the System Under Test, or "SUT" for short. "SUT" is used throughout this article to refer to the ASP.NET Core app being tested.
Don't write integration tests for every permutation of data and file access with databases and file systems. Regardless of how many places across an app interact with databases and file systems, a focused set of read, write, update, and delete integration tests are usually capable of adequately testing database and file system components. Use unit tests for routine tests of method logic that interact with these components. In unit tests, the use of infrastructure fakes or mocks result in faster test execution.
ASP.NET Core integration tests
Integration tests in ASP.NET Core require the following:
A test project is used to contain and execute the tests. The test project has a reference to the SUT.
The test project creates a test web host for the SUT and uses a test server client to handle requests and responses with the SUT.
A test runner is used to execute the tests and report the test results.
Integration tests follow a sequence of events that include the usual Arrange, Act, and Assert test steps:
The SUT's web host is configured.
A test server client is created to submit requests to the app.
The Arrange test step is executed: The test app prepares a request.
The Act test step is executed: The client submits the request and receives the response.
The Assert test step is executed: The actual response is validated as a pass or fail based on an expected response.
The process continues until all of the tests are executed.
The test results are reported.
Usually, the test web host is configured differently than the app's normal web host for the test runs. For example, a different database or different app settings might be used for the tests.
Infrastructure components, such as the test web host and in-memory test server (TestServer), are provided or managed by the Microsoft.AspNetCore.Mvc.Testing package. Use of this package streamlines test creation and execution.
The Microsoft.AspNetCore.Mvc.Testing package handles the following tasks:
Copies the dependencies file (.deps) from the SUT into the test project's bin directory.
Sets the content root to the SUT's project root so that static files and pages/views are found when the tests are executed.
Provides the WebApplicationFactory class to streamline bootstrapping the SUT with TestServer.
The unit tests documentation describes how to set up a test project and test runner, along with detailed instructions on how to run tests and recommendations for how to name tests and test classes.
Separate unit tests from integration tests into different projects. Separating the tests:
Helps ensure that infrastructure testing components aren't accidentally included in the unit tests.
Allows control over which set of tests are run.
There's virtually no difference between the configuration for tests of Razor Pages apps and MVC apps. The only difference is in how the tests are named. In a Razor Pages app, tests of page endpoints are usually named after the page model class (for example, IndexPageTests to test component integration for the Index page). In an MVC app, tests are usually organized by controller classes and named after the controllers they test (for example, HomeControllerTests to test component integration for the Home controller).
Specify the Web SDK in the project file (<Project Sdk="Microsoft.NET.Sdk.Web">).
These prerequisites can be seen in the sample app. Inspect the tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj file. The sample app uses the xUnit test framework and the AngleSharp parser library, so the sample app also references:
Test classes implement a class fixture interface (IClassFixture) to indicate the class contains tests and provide shared object instances across the tests in the class.
The following test class, BasicTests, uses the WebApplicationFactory to bootstrap the SUT and provide an HttpClient to a test method, Get_EndpointsReturnSuccessAndCorrectContentType. The method verifies the response status code is successful (200-299) and the Content-Type header is text/html; charset=utf-8 for several app pages.
CreateClient() creates an instance of HttpClient that automatically follows redirects and handles cookies.
public class BasicTests
: IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public BasicTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
}
[Theory]
[InlineData("/")]
[InlineData("/Index")]
[InlineData("/About")]
[InlineData("/Privacy")]
[InlineData("/Contact")]
public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
{
// Arrange
var client = _factory.CreateClient();
// Act
var response = await client.GetAsync(url);
// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.Equal("text/html; charset=utf-8",
response.Content.Headers.ContentType.ToString());
}
}
By default, non-essential cookies aren't preserved across requests when the General Data Protection Regulation consent policy is enabled. To preserve non-essential cookies, such as those used by the TempData provider, mark them as essential in your tests. For instructions on marking a cookie as essential, see Essential cookies.
AngleSharp vs Application Parts for antiforgery checks
Web host configuration can be created independently of the test classes by inheriting from WebApplicationFactory<TEntryPoint> to create one or more custom factories:
public class CustomWebApplicationFactory<TProgram>
: WebApplicationFactory<TProgram> where TProgram : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
var dbContextDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));
services.Remove(dbContextDescriptor);
var dbConnectionDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbConnection));
services.Remove(dbConnectionDescriptor);
// Create open SqliteConnection so EF won't automatically close it.
services.AddSingleton<DbConnection>(container =>
{
var connection = new SqliteConnection("DataSource=:memory:");
connection.Open();
return connection;
});
services.AddDbContext<ApplicationDbContext>((container, options) =>
{
var connection = container.GetRequiredService<DbConnection>();
options.UseSqlite(connection);
});
});
builder.UseEnvironment("Development");
}
}
The SUT's database context is registered in Program.cs. The test app's builder.ConfigureServices callback is executed after the app's Program.cs code is executed. To use a different database for the tests than the app's database, the app's database context must be replaced in builder.ConfigureServices.
The sample app finds the service descriptor for the database context and uses the descriptor to remove the service registration. The factory then adds a new ApplicationDbContext that uses an in-memory database for the tests..
To connect to a different database, change the DbConnection. To use a SQL Server test database:
The sample app's client is configured to prevent the HttpClient from following redirects. As explained later in the Mock authentication section, this permits tests to check the result of the app's first response. The first response is a redirect in many of these tests with a Location header.
A typical test uses the HttpClient and helper methods to process the request and the response:
[Fact]
public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot()
{
// Arrange
var defaultPage = await _client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
//Act
var response = await _client.SendAsync(
(IHtmlFormElement)content.QuerySelector("form[id='messages']"),
(IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']"));
// Assert
Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("/", response.Headers.Location.OriginalString);
}
Any POST request to the SUT must satisfy the antiforgery check that's automatically made by the app's data protection antiforgery system. In order to arrange for a test's POST request, the test app must:
Make a request for the page.
Parse the antiforgery cookie and request validation token from the response.
Make the POST request with the antiforgery cookie and request validation token in place.
The SendAsync helper extension methods (Helpers/HttpClientExtensions.cs) and the GetDocumentAsync helper method (Helpers/HtmlHelpers.cs) in the sample app use the AngleSharp parser to handle the antiforgery check with the following methods:
GetDocumentAsync: Receives the HttpResponseMessage and returns an IHtmlDocument. GetDocumentAsync uses a factory that prepares a virtual response based on the original HttpResponseMessage. For more information, see the AngleSharp documentation.
SendAsync extension methods for the HttpClient compose an HttpRequestMessage and call SendAsync(HttpRequestMessage) to submit requests to the SUT. Overloads for SendAsync accept the HTML form (IHtmlFormElement) and the following:
Submit button of the form (IHtmlElement)
Form values collection (IEnumerable<KeyValuePair<string, string>>)
Submit button (IHtmlElement) and form values (IEnumerable<KeyValuePair<string, string>>)
AngleSharp is a third-party parsing library used for demonstration purposes in this article and the sample app. AngleSharp isn't supported or required for integration testing of ASP.NET Core apps. Other parsers can be used, such as the Html Agility Pack (HAP). Another approach is to write code to handle the antiforgery system's request verification token and antiforgery cookie directly. See AngleSharp vs Application Parts for antiforgery checks in this article for more information.
When additional configuration is required within a test method, WithWebHostBuilder creates a new WebApplicationFactory with an IWebHostBuilder that is further customized by configuration.
The sample code calls WithWebHostBuilder to replace configured services with test stubs. For more information and example usage, see Inject mock services in this article.
The Post_DeleteMessageHandler_ReturnsRedirectToRoot test method of the sample app demonstrates the use of WithWebHostBuilder. This test performs a record delete in the database by triggering a form submission in the SUT.
Because another test in the IndexPageTests class performs an operation that deletes all of the records in the database and may run before the Post_DeleteMessageHandler_ReturnsRedirectToRoot method, the database is reseeded in this test method to ensure that a record is present for the SUT to delete. Selecting the first delete button of the messages form in the SUT is simulated in the request to the SUT:
[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
// Arrange
using (var scope = _factory.Services.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var db = scopedServices.GetRequiredService<ApplicationDbContext>();
Utilities.ReinitializeDbForTests(db);
}
var defaultPage = await _client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
//Act
var response = await _client.SendAsync(
(IHtmlFormElement)content.QuerySelector("form[id='messages']"),
(IHtmlButtonElement)content.QuerySelector("form[id='messages']")
.QuerySelector("div[class='panel-body']")
.QuerySelector("button"));
// Assert
Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("/", response.Headers.Location.OriginalString);
}
NOTE: To avoid HTTPS redirection warnings in logs when using HTTPS Redirection Middleware, set BaseAddress = new Uri("https://localhost")
Inject mock services
Services can be overridden in a test with a call to ConfigureTestServices on the host builder. To scope the overridden services to the test itself, the WithWebHostBuilder method is used to retrieve a host builder. This can be seen in the following tests:
The sample SUT includes a scoped service that returns a quote. The quote is embedded in a hidden field on the Index page when the Index page is requested.
Services/IQuoteService.cs:
public interface IQuoteService
{
Task<string> GenerateQuote();
}
The following markup is generated when the SUT app is run:
<input id="quote" type="hidden" value="Come on, Sarah. We've an appointment in
London, and we're already 30,000 years late.">
To test the service and quote injection in an integration test, a mock service is injected into the SUT by the test. The mock service replaces the app's QuoteService with a service provided by the test app, called TestQuoteService:
ConfigureTestServices is called, and the scoped service is registered:
[Fact]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddScoped<IQuoteService, TestQuoteService>();
});
})
.CreateClient();
//Act
var defaultPage = await client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
var quoteElement = content.QuerySelector("#quote");
// Assert
Assert.Equal("Something's interfering with time, Mr. Scarman, " +
"and time is my business.", quoteElement.Attributes["value"].Value);
}
The markup produced during the test's execution reflects the quote text supplied by TestQuoteService, thus the assertion passes:
<input id="quote" type="hidden" value="Something's interfering with time,
Mr. Scarman, and time is my business.">
Mock authentication
Tests in the AuthTests class check that a secure endpoint:
Redirects an unauthenticated user to the app's sign in page.
[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
// Arrange
var client = _factory.CreateClient(
new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
// Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.StartsWith("http://localhost/Identity/Account/Login",
response.Headers.Location.OriginalString);
}
By disallowing the client to follow the redirect, the following checks can be made:
The status code returned by the SUT can be checked against the expected HttpStatusCode.Redirect result, not the final status code after the redirect to the sign in page, which would be HttpStatusCode.OK.
The Location header value in the response headers is checked to confirm that it starts with http://localhost/Identity/Account/Login, not the final sign in page response, where the Location header wouldn't be present.
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder)
: base(options, logger, encoder)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "TestScheme");
var result = AuthenticateResult.Success(ticket);
return Task.FromResult(result);
}
}
The TestAuthHandler is called to authenticate a user when the authentication scheme is set to TestScheme where AddAuthentication is registered for ConfigureTestServices. It's important for the TestScheme scheme to match the scheme your app expects. Otherwise, authentication won't work.
For more information on WebApplicationFactoryClientOptions, see the Client options section.
Basic tests for authentication middleware
See this GitHub repository for basic tests of authentication middleware. It contains a test server that’s specific to the test scenario.
Set the environment
Set the environment in the custom application factory:
public class CustomWebApplicationFactory<TProgram>
: WebApplicationFactory<TProgram> where TProgram : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
var dbContextDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));
services.Remove(dbContextDescriptor);
var dbConnectionDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbConnection));
services.Remove(dbConnectionDescriptor);
// Create open SqliteConnection so EF won't automatically close it.
services.AddSingleton<DbConnection>(container =>
{
var connection = new SqliteConnection("DataSource=:memory:");
connection.Open();
return connection;
});
services.AddDbContext<ApplicationDbContext>((container, options) =>
{
var connection = container.GetRequiredService<DbConnection>();
options.UseSqlite(connection);
});
});
builder.UseEnvironment("Development");
}
}
How the test infrastructure infers the app content root path
The WebApplicationFactory constructor infers the app content root path by searching for a WebApplicationFactoryContentRootAttribute on the assembly containing the integration tests with a key equal to the TEntryPoint assembly System.Reflection.Assembly.FullName. In case an attribute with the correct key isn't found, WebApplicationFactory falls back to searching for a solution file (.sln) and appends the TEntryPoint assembly name to the solution directory. The app root directory (the content root path) is used to discover views and content files.
Disable shadow copying
Shadow copying causes the tests to execute in a different directory than the output directory. If your tests rely on loading files relative to Assembly.Location and you encounter issues, you might have to disable shadow copying.
To disable shadow copying when using xUnit, create a xunit.runner.json file in your test project directory, with the correct configuration setting:
{
"shadowCopy": false
}
Disposal of objects
After the tests of the IClassFixture implementation are executed, TestServer and HttpClient are disposed when xUnit disposes of the WebApplicationFactory. If objects instantiated by the developer require disposal, dispose of them in the IClassFixture implementation. For more information, see Implementing a Dispose method.
Allows a user to add, delete one, delete all, and analyze messages.
Test app
tests/RazorPagesProject.Tests
Used to integration test the SUT.
The tests can be run using the built-in test features of an IDE, such as Visual Studio. If using Visual Studio Code or the command line, execute the following command at a command prompt in the tests/RazorPagesProject.Tests directory:
dotnet test
Message app (SUT) organization
The SUT is a Razor Pages message system with the following characteristics:
The Index page of the app (Pages/Index.cshtml and Pages/Index.cshtml.cs) provides a UI and page model methods to control the addition, deletion, and analysis of messages (average words per message).
A message is described by the Message class (Data/Message.cs) with two properties: Id (key) and Text (message). The Text property is required and limited to 200 characters.
The app contains a data access layer (DAL) in its database context class, AppDbContext (Data/AppDbContext.cs).
If the database is empty on app startup, the message store is initialized with three messages.
The app includes a /SecurePage that can only be accessed by an authenticated user.
†The EF article, Test with InMemory, explains how to use an in-memory database for tests with MSTest. This topic uses the xUnit test framework. Test concepts and test implementations across different test frameworks are similar but not identical.
Obtaining a GitHub user profile and checking the profile's user login.
BasicTests
Contains a test method for routing and content type.
IntegrationTests
Contains the integration tests for the Index page using custom WebApplicationFactory class.
Helpers/Utilities
Utilities.cs contains the InitializeDbForTests method used to seed the database with test data.
HtmlHelpers.cs provides a method to return an AngleSharp IHtmlDocument for use by the test methods.
HttpClientExtensions.cs provide overloads for SendAsync to submit requests to the SUT.
The test framework is xUnit. Integration tests are conducted using the Microsoft.AspNetCore.TestHost, which includes the TestServer. Because the Microsoft.AspNetCore.Mvc.Testing package is used to configure the test host and test server, the TestHost and TestServer packages don't require direct package references in the test app's project file or developer configuration in the test app.
Integration tests usually require a small dataset in the database prior to the test execution. For example, a delete test calls for a database record deletion, so the database must have at least one record for the delete request to succeed.
The sample app seeds the database with three messages in Utilities.cs that tests can use when they execute:
public static void InitializeDbForTests(ApplicationDbContext db)
{
db.Messages.AddRange(GetSeedingMessages());
db.SaveChanges();
}
public static void ReinitializeDbForTests(ApplicationDbContext db)
{
db.Messages.RemoveRange(db.Messages);
InitializeDbForTests(db);
}
public static List<Message> GetSeedingMessages()
{
return new List<Message>()
{
new Message(){ Text = "TEST RECORD: You're standing on my scarf." },
new Message(){ Text = "TEST RECORD: Would you like a jelly baby?" },
new Message(){ Text = "TEST RECORD: To the rational mind, " +
"nothing is inexplicable; only unexplained." }
};
}
The SUT's database context is registered in Program.cs. The test app's builder.ConfigureServices callback is executed after the app's Program.cs code is executed. To use a different database for the tests, the app's database context must be replaced in builder.ConfigureServices. For more information, see the Customize WebApplicationFactory section.
For testing SPAs, we recommend a tool such as Playwright for .NET, which can automate a browser.
Introduction to integration tests
Integration tests evaluate an app's components on a broader level than unit tests. Unit tests are used to test isolated software components, such as individual class methods. Integration tests confirm that two or more app components work together to produce an expected result, possibly including every component required to fully process a request.
These broader tests are used to test the app's infrastructure and whole framework, often including the following components:
Database
File system
Network appliances
Request-response pipeline
Unit tests use fabricated components, known as fakes or mock objects, in place of infrastructure components.
In contrast to unit tests, integration tests:
Use the actual components that the app uses in production.
Require more code and data processing.
Take longer to run.
Therefore, limit the use of integration tests to the most important infrastructure scenarios. If a behavior can be tested using either a unit test or an integration test, choose the unit test.
In discussions of integration tests, the tested project is frequently called the System Under Test, or "SUT" for short. "SUT" is used throughout this article to refer to the ASP.NET Core app being tested.
Don't write integration tests for every permutation of data and file access with databases and file systems. Regardless of how many places across an app interact with databases and file systems, a focused set of read, write, update, and delete integration tests are usually capable of adequately testing database and file system components. Use unit tests for routine tests of method logic that interact with these components. In unit tests, the use of infrastructure fakes or mocks result in faster test execution.
ASP.NET Core integration tests
Integration tests in ASP.NET Core require the following:
A test project is used to contain and execute the tests. The test project has a reference to the SUT.
The test project creates a test web host for the SUT and uses a test server client to handle requests and responses with the SUT.
A test runner is used to execute the tests and report the test results.
Integration tests follow a sequence of events that include the usual Arrange, Act, and Assert test steps:
The SUT's web host is configured.
A test server client is created to submit requests to the app.
The Arrange test step is executed: The test app prepares a request.
The Act test step is executed: The client submits the request and receives the response.
The Assert test step is executed: The actual response is validated as a pass or fail based on an expected response.
The process continues until all of the tests are executed.
The test results are reported.
Usually, the test web host is configured differently than the app's normal web host for the test runs. For example, a different database or different app settings might be used for the tests.
Infrastructure components, such as the test web host and in-memory test server (TestServer), are provided or managed by the Microsoft.AspNetCore.Mvc.Testing package. Use of this package streamlines test creation and execution.
The Microsoft.AspNetCore.Mvc.Testing package handles the following tasks:
Copies the dependencies file (.deps) from the SUT into the test project's bin directory.
Sets the content root to the SUT's project root so that static files and pages/views are found when the tests are executed.
Provides the WebApplicationFactory class to streamline bootstrapping the SUT with TestServer.
The unit tests documentation describes how to set up a test project and test runner, along with detailed instructions on how to run tests and recommendations for how to name tests and test classes.
Separate unit tests from integration tests into different projects. Separating the tests:
Helps ensure that infrastructure testing components aren't accidentally included in the unit tests.
Allows control over which set of tests are run.
There's virtually no difference between the configuration for tests of Razor Pages apps and MVC apps. The only difference is in how the tests are named. In a Razor Pages app, tests of page endpoints are usually named after the page model class (for example, IndexPageTests to test component integration for the Index page). In an MVC app, tests are usually organized by controller classes and named after the controllers they test (for example, HomeControllerTests to test component integration for the Home controller).
Specify the Web SDK in the project file (<Project Sdk="Microsoft.NET.Sdk.Web">).
These prerequisites can be seen in the sample app. Inspect the tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj file. The sample app uses the xUnit test framework and the AngleSharp parser library, so the sample app also references:
Test classes implement a class fixture interface (IClassFixture) to indicate the class contains tests and provide shared object instances across the tests in the class.
The following test class, BasicTests, uses the WebApplicationFactory to bootstrap the SUT and provide an HttpClient to a test method, Get_EndpointsReturnSuccessAndCorrectContentType. The method checks if the response status code is successful (status codes in the range 200-299) and the Content-Type header is text/html; charset=utf-8 for several app pages.
CreateClient() creates an instance of HttpClient that automatically follows redirects and handles cookies.
public class BasicTests
: IClassFixture<WebApplicationFactory<RazorPagesProject.Startup>>
{
private readonly WebApplicationFactory<RazorPagesProject.Startup> _factory;
public BasicTests(WebApplicationFactory<RazorPagesProject.Startup> factory)
{
_factory = factory;
}
[Theory]
[InlineData("/")]
[InlineData("/Index")]
[InlineData("/About")]
[InlineData("/Privacy")]
[InlineData("/Contact")]
public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
{
// Arrange
var client = _factory.CreateClient();
// Act
var response = await client.GetAsync(url);
// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.Equal("text/html; charset=utf-8",
response.Content.Headers.ContentType.ToString());
}
}
By default, non-essential cookies aren't preserved across requests when the GDPR consent policy is enabled. To preserve non-essential cookies, such as those used by the TempData provider, mark them as essential in your tests. For instructions on marking a cookie as essential, see Essential cookies.
Customize WebApplicationFactory
Web host configuration can be created independently of the test classes by inheriting from WebApplicationFactory to create one or more custom factories:
The SUT's database context is registered in its Startup.ConfigureServices method. The test app's builder.ConfigureServices callback is executed after the app's Startup.ConfigureServices code is executed. The execution order is a breaking change for the Generic Host with the release of ASP.NET Core 3.0. To use a different database for the tests than the app's database, the app's database context must be replaced in builder.ConfigureServices.
For SUTs that still use the Web Host, the test app's builder.ConfigureServices callback is executed before the SUT's Startup.ConfigureServices code. The test app's builder.ConfigureTestServices callback is executed after.
The sample app finds the service descriptor for the database context and uses the descriptor to remove the service registration. Next, the factory adds a new ApplicationDbContext that uses an in-memory database for the tests.
To connect to a different database than the in-memory database, change the UseInMemoryDatabase call to connect the context to a different database. To use a SQL Server test database:
The sample app's client is configured to prevent the HttpClient from following redirects. As explained later in the Mock authentication section, this permits tests to check the result of the app's first response. The first response is a redirect in many of these tests with a Location header.
A typical test uses the HttpClient and helper methods to process the request and the response:
[Fact]
public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot()
{
// Arrange
var defaultPage = await _client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
//Act
var response = await _client.SendAsync(
(IHtmlFormElement)content.QuerySelector("form[id='messages']"),
(IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']"));
// Assert
Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("/", response.Headers.Location.OriginalString);
}
Any POST request to the SUT must satisfy the antiforgery check that's automatically made by the app's data protection antiforgery system. In order to arrange for a test's POST request, the test app must:
Make a request for the page.
Parse the antiforgery cookie and request validation token from the response.
Make the POST request with the antiforgery cookie and request validation token in place.
The SendAsync helper extension methods (Helpers/HttpClientExtensions.cs) and the GetDocumentAsync helper method (Helpers/HtmlHelpers.cs) in the sample app use the AngleSharp parser to handle the antiforgery check with the following methods:
GetDocumentAsync: Receives the HttpResponseMessage and returns an IHtmlDocument. GetDocumentAsync uses a factory that prepares a virtual response based on the original HttpResponseMessage. For more information, see the AngleSharp documentation.
SendAsync extension methods for the HttpClient compose an HttpRequestMessage and call SendAsync(HttpRequestMessage) to submit requests to the SUT. Overloads for SendAsync accept the HTML form (IHtmlFormElement) and the following:
Submit button of the form (IHtmlElement)
Form values collection (IEnumerable<KeyValuePair<string, string>>)
Submit button (IHtmlElement) and form values (IEnumerable<KeyValuePair<string, string>>)
Note
AngleSharp is a third-party parsing library used for demonstration purposes in this topic and the sample app. AngleSharp isn't supported or required for integration testing of ASP.NET Core apps. Other parsers can be used, such as the Html Agility Pack (HAP). Another approach is to write code to handle the antiforgery system's request verification token and antiforgery cookie directly.
When additional configuration is required within a test method, WithWebHostBuilder creates a new WebApplicationFactory with an IWebHostBuilder that is further customized by configuration.
The Post_DeleteMessageHandler_ReturnsRedirectToRoot test method of the sample app demonstrates the use of WithWebHostBuilder. This test performs a record delete in the database by triggering a form submission in the SUT.
Because another test in the IndexPageTests class performs an operation that deletes all of the records in the database and may run before the Post_DeleteMessageHandler_ReturnsRedirectToRoot method, the database is reseeded in this test method to ensure that a record is present for the SUT to delete. Selecting the first delete button of the messages form in the SUT is simulated in the request to the SUT:
[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
var serviceProvider = services.BuildServiceProvider();
using (var scope = serviceProvider.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var db = scopedServices
.GetRequiredService<ApplicationDbContext>();
var logger = scopedServices
.GetRequiredService<ILogger<IndexPageTests>>();
try
{
Utilities.ReinitializeDbForTests(db);
}
catch (Exception ex)
{
logger.LogError(ex, "An error occurred seeding " +
"the database with test messages. Error: {Message}",
ex.Message);
}
}
});
})
.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
var defaultPage = await client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
//Act
var response = await client.SendAsync(
(IHtmlFormElement)content.QuerySelector("form[id='messages']"),
(IHtmlButtonElement)content.QuerySelector("form[id='messages']")
.QuerySelector("div[class='panel-body']")
.QuerySelector("button"));
// Assert
Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("/", response.Headers.Location.OriginalString);
}
Gets or sets the maximum number of redirect responses that HttpClient instances should follow.
7
Create the WebApplicationFactoryClientOptions class and pass it to the CreateClient() method (default values are shown in the code example):
// Default client option values are shown
var clientOptions = new WebApplicationFactoryClientOptions();
clientOptions.AllowAutoRedirect = true;
clientOptions.BaseAddress = new Uri("http://localhost");
clientOptions.HandleCookies = true;
clientOptions.MaxAutomaticRedirections = 7;
_client = _factory.CreateClient(clientOptions);
Inject mock services
Services can be overridden in a test with a call to ConfigureTestServices on the host builder. To inject mock services, the SUT must have a Startup class with a Startup.ConfigureServices method.
The sample SUT includes a scoped service that returns a quote. The quote is embedded in a hidden field on the Index page when the Index page is requested.
Services/IQuoteService.cs:
public interface IQuoteService
{
Task<string> GenerateQuote();
}
The following markup is generated when the SUT app is run:
<input id="quote" type="hidden" value="Come on, Sarah. We've an appointment in
London, and we're already 30,000 years late.">
To test the service and quote injection in an integration test, a mock service is injected into the SUT by the test. The mock service replaces the app's QuoteService with a service provided by the test app, called TestQuoteService:
ConfigureTestServices is called, and the scoped service is registered:
[Fact]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddScoped<IQuoteService, TestQuoteService>();
});
})
.CreateClient();
//Act
var defaultPage = await client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
var quoteElement = content.QuerySelector("#quote");
// Assert
Assert.Equal("Something's interfering with time, Mr. Scarman, " +
"and time is my business.", quoteElement.Attributes["value"].Value);
}
The markup produced during the test's execution reflects the quote text supplied by TestQuoteService, thus the assertion passes:
<input id="quote" type="hidden" value="Something's interfering with time,
Mr. Scarman, and time is my business.">
Mock authentication
Tests in the AuthTests class check that a secure endpoint:
Redirects an unauthenticated user to the app's Login page.
[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
// Arrange
var client = _factory.CreateClient(
new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
// Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.StartsWith("http://localhost/Identity/Account/Login",
response.Headers.Location.OriginalString);
}
By disallowing the client to follow the redirect, the following checks can be made:
The status code returned by the SUT can be checked against the expected HttpStatusCode.Redirect result, not the final status code after the redirect to the Login page, which would be HttpStatusCode.OK.
The Location header value in the response headers is checked to confirm that it starts with http://localhost/Identity/Account/Login, not the final Login page response, where the Location header wouldn't be present.
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "Test");
var result = AuthenticateResult.Success(ticket);
return Task.FromResult(result);
}
}
The TestAuthHandler is called to authenticate a user when the authentication scheme is set to Test where AddAuthentication is registered for ConfigureTestServices. It's important for the Test scheme to match the scheme your app expects. Otherwise, authentication won't work.
For more information on WebApplicationFactoryClientOptions, see the Client options section.
Set the environment
By default, the SUT's host and app environment is configured to use the Development environment. To override the SUT's environment when using IHostBuilder:
Set the ASPNETCORE_ENVIRONMENT environment variable (for example, Staging, Production, or other custom value, such as Testing).
Override CreateHostBuilder in the test app to read environment variables prefixed with ASPNETCORE.
How the test infrastructure infers the app content root path
The WebApplicationFactory constructor infers the app content root path by searching for a WebApplicationFactoryContentRootAttribute on the assembly containing the integration tests with a key equal to the TEntryPoint assembly System.Reflection.Assembly.FullName. In case an attribute with the correct key isn't found, WebApplicationFactory falls back to searching for a solution file (.sln) and appends the TEntryPoint assembly name to the solution directory. The app root directory (the content root path) is used to discover views and content files.
Disable shadow copying
Shadow copying causes the tests to execute in a different directory than the output directory. If your tests rely on loading files relative to Assembly.Location and you encounter issues, you might have to disable shadow copying.
To disable shadow copying when using xUnit, create a xunit.runner.json file in your test project directory, with the correct configuration setting:
{
"shadowCopy": false
}
Disposal of objects
After the tests of the IClassFixture implementation are executed, TestServer and HttpClient are disposed when xUnit disposes of the WebApplicationFactory. If objects instantiated by the developer require disposal, dispose of them in the IClassFixture implementation. For more information, see Implementing a Dispose method.
Allows a user to add, delete one, delete all, and analyze messages.
Test app
tests/RazorPagesProject.Tests
Used to integration test the SUT.
The tests can be run using the built-in test features of an IDE, such as Visual Studio. If using Visual Studio Code or the command line, execute the following command at a command prompt in the tests/RazorPagesProject.Tests directory:
dotnet test
Message app (SUT) organization
The SUT is a Razor Pages message system with the following characteristics:
The Index page of the app (Pages/Index.cshtml and Pages/Index.cshtml.cs) provides a UI and page model methods to control the addition, deletion, and analysis of messages (average words per message).
A message is described by the Message class (Data/Message.cs) with two properties: Id (key) and Text (message). The Text property is required and limited to 200 characters.
The app contains a data access layer (DAL) in its database context class, AppDbContext (Data/AppDbContext.cs).
If the database is empty on app startup, the message store is initialized with three messages.
The app includes a /SecurePage that can only be accessed by an authenticated user.
†The EF topic, Test with InMemory, explains how to use an in-memory database for tests with MSTest. This topic uses the xUnit test framework. Test concepts and test implementations across different test frameworks are similar but not identical.
Obtaining a GitHub user profile and checking the profile's user login.
BasicTests
Contains a test method for routing and content type.
IntegrationTests
Contains the integration tests for the Index page using custom WebApplicationFactory class.
Helpers/Utilities
Utilities.cs contains the InitializeDbForTests method used to seed the database with test data.
HtmlHelpers.cs provides a method to return an AngleSharp IHtmlDocument for use by the test methods.
HttpClientExtensions.cs provide overloads for SendAsync to submit requests to the SUT.
The test framework is xUnit. Integration tests are conducted using the Microsoft.AspNetCore.TestHost, which includes the TestServer. Because the Microsoft.AspNetCore.Mvc.Testing package is used to configure the test host and test server, the TestHost and TestServer packages don't require direct package references in the test app's project file or developer configuration in the test app.
Integration tests usually require a small dataset in the database prior to the test execution. For example, a delete test calls for a database record deletion, so the database must have at least one record for the delete request to succeed.
The sample app seeds the database with three messages in Utilities.cs that tests can use when they execute:
public static void InitializeDbForTests(ApplicationDbContext db)
{
db.Messages.AddRange(GetSeedingMessages());
db.SaveChanges();
}
public static void ReinitializeDbForTests(ApplicationDbContext db)
{
db.Messages.RemoveRange(db.Messages);
InitializeDbForTests(db);
}
public static List<Message> GetSeedingMessages()
{
return new List<Message>()
{
new Message(){ Text = "TEST RECORD: You're standing on my scarf." },
new Message(){ Text = "TEST RECORD: Would you like a jelly baby?" },
new Message(){ Text = "TEST RECORD: To the rational mind, " +
"nothing is inexplicable; only unexplained." }
};
}
The SUT's database context is registered in its Startup.ConfigureServices method. The test app's builder.ConfigureServices callback is executed after the app's Startup.ConfigureServices code is executed. To use a different database for the tests, the app's database context must be replaced in builder.ConfigureServices. For more information, see the Customize WebApplicationFactory section.
For SUTs that still use the Web Host, the test app's builder.ConfigureServices callback is executed before the SUT's Startup.ConfigureServices code. The test app's builder.ConfigureTestServices callback is executed after.
This article assumes a basic understanding of unit tests. If unfamiliar with test concepts, see the Unit Testing in .NET Core and .NET Standard article and its linked content.
The sample app is a Razor Pages app and assumes a basic understanding of Razor Pages. If you're unfamiliar with Razor Pages, see the following articles:
For testing SPAs, we recommend a tool such as Playwright for .NET, which can automate a browser.
Introduction to integration tests
Integration tests evaluate an app's components on a broader level than unit tests. Unit tests are used to test isolated software components, such as individual class methods. Integration tests confirm that two or more app components work together to produce an expected result, possibly including every component required to fully process a request.
These broader tests are used to test the app's infrastructure and whole framework, often including the following components:
Database
File system
Network appliances
Request-response pipeline
Unit tests use fabricated components, known as fakes or mock objects, in place of infrastructure components.
In contrast to unit tests, integration tests:
Use the actual components that the app uses in production.
Require more code and data processing.
Take longer to run.
Therefore, limit the use of integration tests to the most important infrastructure scenarios. If a behavior can be tested using either a unit test or an integration test, choose the unit test.
In discussions of integration tests, the tested project is frequently called the System Under Test, or "SUT" for short. "SUT" is used throughout this article to refer to the ASP.NET Core app being tested.
Don't write integration tests for every permutation of data and file access with databases and file systems. Regardless of how many places across an app interact with databases and file systems, a focused set of read, write, update, and delete integration tests are usually capable of adequately testing database and file system components. Use unit tests for routine tests of method logic that interact with these components. In unit tests, the use of infrastructure fakes or mocks result in faster test execution.
ASP.NET Core integration tests
Integration tests in ASP.NET Core require the following:
A test project is used to contain and execute the tests. The test project has a reference to the SUT.
The test project creates a test web host for the SUT and uses a test server client to handle requests and responses with the SUT.
A test runner is used to execute the tests and report the test results.
Integration tests follow a sequence of events that include the usual Arrange, Act, and Assert test steps:
The SUT's web host is configured.
A test server client is created to submit requests to the app.
The Arrange test step is executed: The test app prepares a request.
The Act test step is executed: The client submits the request and receives the response.
The Assert test step is executed: The actual response is validated as a pass or fail based on an expected response.
The process continues until all of the tests are executed.
The test results are reported.
Usually, the test web host is configured differently than the app's normal web host for the test runs. For example, a different database or different app settings might be used for the tests.
Infrastructure components, such as the test web host and in-memory test server (TestServer), are provided or managed by the Microsoft.AspNetCore.Mvc.Testing package. Use of this package streamlines test creation and execution.
The Microsoft.AspNetCore.Mvc.Testing package handles the following tasks:
Copies the dependencies file (.deps) from the SUT into the test project's bin directory.
Sets the content root to the SUT's project root so that static files and pages/views are found when the tests are executed.
Provides the WebApplicationFactory class to streamline bootstrapping the SUT with TestServer.
The unit tests documentation describes how to set up a test project and test runner, along with detailed instructions on how to run tests and recommendations for how to name tests and test classes.
Separate unit tests from integration tests into different projects. Separating the tests:
Helps ensure that infrastructure testing components aren't accidentally included in the unit tests.
Allows control over which set of tests are run.
There's virtually no difference between the configuration for tests of Razor Pages apps and MVC apps. The only difference is in how the tests are named. In a Razor Pages app, tests of page endpoints are usually named after the page model class (for example, IndexPageTests to test component integration for the Index page). In an MVC app, tests are usually organized by controller classes and named after the controllers they test (for example, HomeControllerTests to test component integration for the Home controller).
Specify the Web SDK in the project file (<Project Sdk="Microsoft.NET.Sdk.Web">).
These prerequisites can be seen in the sample app. Inspect the tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj file. The sample app uses the xUnit test framework and the AngleSharp parser library, so the sample app also references:
Test classes implement a class fixture interface (IClassFixture) to indicate the class contains tests and provide shared object instances across the tests in the class.
The following test class, BasicTests, uses the WebApplicationFactory to bootstrap the SUT and provide an HttpClient to a test method, Get_EndpointsReturnSuccessAndCorrectContentType. The method verifies the response status code is successful (200-299) and the Content-Type header is text/html; charset=utf-8 for several app pages.
CreateClient() creates an instance of HttpClient that automatically follows redirects and handles cookies.
public class BasicTests
: IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public BasicTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
}
[Theory]
[InlineData("/")]
[InlineData("/Index")]
[InlineData("/About")]
[InlineData("/Privacy")]
[InlineData("/Contact")]
public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
{
// Arrange
var client = _factory.CreateClient();
// Act
var response = await client.GetAsync(url);
// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.Equal("text/html; charset=utf-8",
response.Content.Headers.ContentType.ToString());
}
}
By default, non-essential cookies aren't preserved across requests when the General Data Protection Regulation consent policy is enabled. To preserve non-essential cookies, such as those used by the TempData provider, mark them as essential in your tests. For instructions on marking a cookie as essential, see Essential cookies.
AngleSharp vs Application Parts for antiforgery checks
Web host configuration can be created independently of the test classes by inheriting from WebApplicationFactory<TEntryPoint> to create one or more custom factories:
public class CustomWebApplicationFactory<TProgram>
: WebApplicationFactory<TProgram> where TProgram : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
var dbContextDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbContextOptions<ApplicationDbContext>));
services.Remove(dbContextDescriptor);
var dbConnectionDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbConnection));
services.Remove(dbConnectionDescriptor);
// Create open SqliteConnection so EF won't automatically close it.
services.AddSingleton<DbConnection>(container =>
{
var connection = new SqliteConnection("DataSource=:memory:");
connection.Open();
return connection;
});
services.AddDbContext<ApplicationDbContext>((container, options) =>
{
var connection = container.GetRequiredService<DbConnection>();
options.UseSqlite(connection);
});
});
builder.UseEnvironment("Development");
}
}
The SUT's database context is registered in Program.cs. The test app's builder.ConfigureServices callback is executed after the app's Program.cs code is executed. To use a different database for the tests than the app's database, the app's database context must be replaced in builder.ConfigureServices.
The sample app finds the service descriptor for the database context and uses the descriptor to remove the service registration. The factory then adds a new ApplicationDbContext that uses an in-memory database for the tests..
To connect to a different database, change the DbConnection. To use a SQL Server test database:
The sample app's client is configured to prevent the HttpClient from following redirects. As explained later in the Mock authentication section, this permits tests to check the result of the app's first response. The first response is a redirect in many of these tests with a Location header.
A typical test uses the HttpClient and helper methods to process the request and the response:
[Fact]
public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot()
{
// Arrange
var defaultPage = await _client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
//Act
var response = await _client.SendAsync(
(IHtmlFormElement)content.QuerySelector("form[id='messages']"),
(IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']"));
// Assert
Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("/", response.Headers.Location.OriginalString);
}
Any POST request to the SUT must satisfy the antiforgery check that's automatically made by the app's data protection antiforgery system. In order to arrange for a test's POST request, the test app must:
Make a request for the page.
Parse the antiforgery cookie and request validation token from the response.
Make the POST request with the antiforgery cookie and request validation token in place.
The SendAsync helper extension methods (Helpers/HttpClientExtensions.cs) and the GetDocumentAsync helper method (Helpers/HtmlHelpers.cs) in the sample app use the AngleSharp parser to handle the antiforgery check with the following methods:
GetDocumentAsync: Receives the HttpResponseMessage and returns an IHtmlDocument. GetDocumentAsync uses a factory that prepares a virtual response based on the original HttpResponseMessage. For more information, see the AngleSharp documentation.
SendAsync extension methods for the HttpClient compose an HttpRequestMessage and call SendAsync(HttpRequestMessage) to submit requests to the SUT. Overloads for SendAsync accept the HTML form (IHtmlFormElement) and the following:
Submit button of the form (IHtmlElement)
Form values collection (IEnumerable<KeyValuePair<string, string>>)
Submit button (IHtmlElement) and form values (IEnumerable<KeyValuePair<string, string>>)
AngleSharp is a third-party parsing library used for demonstration purposes in this article and the sample app. AngleSharp isn't supported or required for integration testing of ASP.NET Core apps. Other parsers can be used, such as the Html Agility Pack (HAP). Another approach is to write code to handle the antiforgery system's request verification token and antiforgery cookie directly. See AngleSharp vs Application Parts for antiforgery checks in this article for more information.
When additional configuration is required within a test method, WithWebHostBuilder creates a new WebApplicationFactory with an IWebHostBuilder that is further customized by configuration.
The sample code calls WithWebHostBuilder to replace configured services with test stubs. For more information and example usage, see Inject mock services in this article.
The Post_DeleteMessageHandler_ReturnsRedirectToRoot test method of the sample app demonstrates the use of WithWebHostBuilder. This test performs a record delete in the database by triggering a form submission in the SUT.
Because another test in the IndexPageTests class performs an operation that deletes all of the records in the database and may run before the Post_DeleteMessageHandler_ReturnsRedirectToRoot method, the database is reseeded in this test method to ensure that a record is present for the SUT to delete. Selecting the first delete button of the messages form in the SUT is simulated in the request to the SUT:
[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
// Arrange
using (var scope = _factory.Services.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var db = scopedServices.GetRequiredService<ApplicationDbContext>();
Utilities.ReinitializeDbForTests(db);
}
var defaultPage = await _client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
//Act
var response = await _client.SendAsync(
(IHtmlFormElement)content.QuerySelector("form[id='messages']"),
(IHtmlButtonElement)content.QuerySelector("form[id='messages']")
.QuerySelector("div[class='panel-body']")
.QuerySelector("button"));
// Assert
Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("/", response.Headers.Location.OriginalString);
}
NOTE: To avoid HTTPS redirection warnings in logs when using HTTPS Redirection Middleware, set BaseAddress = new Uri("https://localhost")
Inject mock services
Services can be overridden in a test with a call to ConfigureTestServices on the host builder. To scope the overridden services to the test itself, the WithWebHostBuilder method is used to retrieve a host builder. This can be seen in the following tests:
The sample SUT includes a scoped service that returns a quote. The quote is embedded in a hidden field on the Index page when the Index page is requested.
Services/IQuoteService.cs:
public interface IQuoteService
{
Task<string> GenerateQuote();
}
The following markup is generated when the SUT app is run:
<input id="quote" type="hidden" value="Come on, Sarah. We've an appointment in
London, and we're already 30,000 years late.">
To test the service and quote injection in an integration test, a mock service is injected into the SUT by the test. The mock service replaces the app's QuoteService with a service provided by the test app, called TestQuoteService:
ConfigureTestServices is called, and the scoped service is registered:
[Fact]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddScoped<IQuoteService, TestQuoteService>();
});
})
.CreateClient();
//Act
var defaultPage = await client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
var quoteElement = content.QuerySelector("#quote");
// Assert
Assert.Equal("Something's interfering with time, Mr. Scarman, " +
"and time is my business.", quoteElement.Attributes["value"].Value);
}
The markup produced during the test's execution reflects the quote text supplied by TestQuoteService, thus the assertion passes:
<input id="quote" type="hidden" value="Something's interfering with time,
Mr. Scarman, and time is my business.">
Mock authentication
Tests in the AuthTests class check that a secure endpoint:
Redirects an unauthenticated user to the app's sign in page.
[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
// Arrange
var client = _factory.CreateClient(
new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
// Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.StartsWith("http://localhost/Identity/Account/Login",
response.Headers.Location.OriginalString);
}
By disallowing the client to follow the redirect, the following checks can be made:
The status code returned by the SUT can be checked against the expected HttpStatusCode.Redirect result, not the final status code after the redirect to the sign in page, which would be HttpStatusCode.OK.
The Location header value in the response headers is checked to confirm that it starts with http://localhost/Identity/Account/Login, not the final sign in page response, where the Location header wouldn't be present.
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "TestScheme");
var result = AuthenticateResult.Success(ticket);
return Task.FromResult(result);
}
}
The TestAuthHandler is called to authenticate a user when the authentication scheme is set to TestScheme where AddAuthentication is registered for ConfigureTestServices. It's important for the TestScheme scheme to match the scheme your app expects. Otherwise, authentication won't work.
For more information on WebApplicationFactoryClientOptions, see the Client options section.
Basic tests for authentication middleware
See this GitHub repository for basic tests of authentication middleware. It contains a test server that’s specific to the test scenario.
Set the environment
Set the environment in the custom application factory:
public class CustomWebApplicationFactory<TProgram>
: WebApplicationFactory<TProgram> where TProgram : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
var dbContextDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbContextOptions<ApplicationDbContext>));
services.Remove(dbContextDescriptor);
var dbConnectionDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbConnection));
services.Remove(dbConnectionDescriptor);
// Create open SqliteConnection so EF won't automatically close it.
services.AddSingleton<DbConnection>(container =>
{
var connection = new SqliteConnection("DataSource=:memory:");
connection.Open();
return connection;
});
services.AddDbContext<ApplicationDbContext>((container, options) =>
{
var connection = container.GetRequiredService<DbConnection>();
options.UseSqlite(connection);
});
});
builder.UseEnvironment("Development");
}
}
How the test infrastructure infers the app content root path
The WebApplicationFactory constructor infers the app content root path by searching for a WebApplicationFactoryContentRootAttribute on the assembly containing the integration tests with a key equal to the TEntryPoint assembly System.Reflection.Assembly.FullName. In case an attribute with the correct key isn't found, WebApplicationFactory falls back to searching for a solution file (.sln) and appends the TEntryPoint assembly name to the solution directory. The app root directory (the content root path) is used to discover views and content files.
Disable shadow copying
Shadow copying causes the tests to execute in a different directory than the output directory. If your tests rely on loading files relative to Assembly.Location and you encounter issues, you might have to disable shadow copying.
To disable shadow copying when using xUnit, create a xunit.runner.json file in your test project directory, with the correct configuration setting:
{
"shadowCopy": false
}
Disposal of objects
After the tests of the IClassFixture implementation are executed, TestServer and HttpClient are disposed when xUnit disposes of the WebApplicationFactory. If objects instantiated by the developer require disposal, dispose of them in the IClassFixture implementation. For more information, see Implementing a Dispose method.
Allows a user to add, delete one, delete all, and analyze messages.
Test app
tests/RazorPagesProject.Tests
Used to integration test the SUT.
The tests can be run using the built-in test features of an IDE, such as Visual Studio. If using Visual Studio Code or the command line, execute the following command at a command prompt in the tests/RazorPagesProject.Tests directory:
dotnet test
Message app (SUT) organization
The SUT is a Razor Pages message system with the following characteristics:
The Index page of the app (Pages/Index.cshtml and Pages/Index.cshtml.cs) provides a UI and page model methods to control the addition, deletion, and analysis of messages (average words per message).
A message is described by the Message class (Data/Message.cs) with two properties: Id (key) and Text (message). The Text property is required and limited to 200 characters.
The app contains a data access layer (DAL) in its database context class, AppDbContext (Data/AppDbContext.cs).
If the database is empty on app startup, the message store is initialized with three messages.
The app includes a /SecurePage that can only be accessed by an authenticated user.
†The EF article, Test with InMemory, explains how to use an in-memory database for tests with MSTest. This topic uses the xUnit test framework. Test concepts and test implementations across different test frameworks are similar but not identical.
Obtaining a GitHub user profile and checking the profile's user login.
BasicTests
Contains a test method for routing and content type.
IntegrationTests
Contains the integration tests for the Index page using custom WebApplicationFactory class.
Helpers/Utilities
Utilities.cs contains the InitializeDbForTests method used to seed the database with test data.
HtmlHelpers.cs provides a method to return an AngleSharp IHtmlDocument for use by the test methods.
HttpClientExtensions.cs provide overloads for SendAsync to submit requests to the SUT.
The test framework is xUnit. Integration tests are conducted using the Microsoft.AspNetCore.TestHost, which includes the TestServer. Because the Microsoft.AspNetCore.Mvc.Testing package is used to configure the test host and test server, the TestHost and TestServer packages don't require direct package references in the test app's project file or developer configuration in the test app.
Integration tests usually require a small dataset in the database prior to the test execution. For example, a delete test calls for a database record deletion, so the database must have at least one record for the delete request to succeed.
The sample app seeds the database with three messages in Utilities.cs that tests can use when they execute:
public static void InitializeDbForTests(ApplicationDbContext db)
{
db.Messages.AddRange(GetSeedingMessages());
db.SaveChanges();
}
public static void ReinitializeDbForTests(ApplicationDbContext db)
{
db.Messages.RemoveRange(db.Messages);
InitializeDbForTests(db);
}
public static List<Message> GetSeedingMessages()
{
return new List<Message>()
{
new Message(){ Text = "TEST RECORD: You're standing on my scarf." },
new Message(){ Text = "TEST RECORD: Would you like a jelly baby?" },
new Message(){ Text = "TEST RECORD: To the rational mind, " +
"nothing is inexplicable; only unexplained." }
};
}
The SUT's database context is registered in Program.cs. The test app's builder.ConfigureServices callback is executed after the app's Program.cs code is executed. To use a different database for the tests, the app's database context must be replaced in builder.ConfigureServices. For more information, see the Customize WebApplicationFactory section.
This article assumes a basic understanding of unit tests. If unfamiliar with test concepts, see the Unit Testing in .NET Core and .NET Standard article and its linked content.
The sample app is a Razor Pages app and assumes a basic understanding of Razor Pages. If you're unfamiliar with Razor Pages, see the following articles:
For testing SPAs, we recommend a tool such as Playwright for .NET, which can automate a browser.
Introduction to integration tests
Integration tests evaluate an app's components on a broader level than unit tests. Unit tests are used to test isolated software components, such as individual class methods. Integration tests confirm that two or more app components work together to produce an expected result, possibly including every component required to fully process a request.
These broader tests are used to test the app's infrastructure and whole framework, often including the following components:
Database
File system
Network appliances
Request-response pipeline
Unit tests use fabricated components, known as fakes or mock objects, in place of infrastructure components.
In contrast to unit tests, integration tests:
Use the actual components that the app uses in production.
Require more code and data processing.
Take longer to run.
Therefore, limit the use of integration tests to the most important infrastructure scenarios. If a behavior can be tested using either a unit test or an integration test, choose the unit test.
In discussions of integration tests, the tested project is frequently called the System Under Test, or "SUT" for short. "SUT" is used throughout this article to refer to the ASP.NET Core app being tested.
Don't write integration tests for every permutation of data and file access with databases and file systems. Regardless of how many places across an app interact with databases and file systems, a focused set of read, write, update, and delete integration tests are usually capable of adequately testing database and file system components. Use unit tests for routine tests of method logic that interact with these components. In unit tests, the use of infrastructure fakes or mocks result in faster test execution.
ASP.NET Core integration tests
Integration tests in ASP.NET Core require the following:
A test project is used to contain and execute the tests. The test project has a reference to the SUT.
The test project creates a test web host for the SUT and uses a test server client to handle requests and responses with the SUT.
A test runner is used to execute the tests and report the test results.
Integration tests follow a sequence of events that include the usual Arrange, Act, and Assert test steps:
The SUT's web host is configured.
A test server client is created to submit requests to the app.
The Arrange test step is executed: The test app prepares a request.
The Act test step is executed: The client submits the request and receives the response.
The Assert test step is executed: The actual response is validated as a pass or fail based on an expected response.
The process continues until all of the tests are executed.
The test results are reported.
Usually, the test web host is configured differently than the app's normal web host for the test runs. For example, a different database or different app settings might be used for the tests.
Infrastructure components, such as the test web host and in-memory test server (TestServer), are provided or managed by the Microsoft.AspNetCore.Mvc.Testing package. Use of this package streamlines test creation and execution.
The Microsoft.AspNetCore.Mvc.Testing package handles the following tasks:
Copies the dependencies file (.deps) from the SUT into the test project's bin directory.
Sets the content root to the SUT's project root so that static files and pages/views are found when the tests are executed.
Provides the WebApplicationFactory class to streamline bootstrapping the SUT with TestServer.
The unit tests documentation describes how to set up a test project and test runner, along with detailed instructions on how to run tests and recommendations for how to name tests and test classes.
Separate unit tests from integration tests into different projects. Separating the tests:
Helps ensure that infrastructure testing components aren't accidentally included in the unit tests.
Allows control over which set of tests are run.
There's virtually no difference between the configuration for tests of Razor Pages apps and MVC apps. The only difference is in how the tests are named. In a Razor Pages app, tests of page endpoints are usually named after the page model class (for example, IndexPageTests to test component integration for the Index page). In an MVC app, tests are usually organized by controller classes and named after the controllers they test (for example, HomeControllerTests to test component integration for the Home controller).
Specify the Web SDK in the project file (<Project Sdk="Microsoft.NET.Sdk.Web">).
These prerequisites can be seen in the sample app. Inspect the tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj file. The sample app uses the xUnit test framework and the AngleSharp parser library, so the sample app also references:
Test classes implement a class fixture interface (IClassFixture) to indicate the class contains tests and provide shared object instances across the tests in the class.
The following test class, BasicTests, uses the WebApplicationFactory to bootstrap the SUT and provide an HttpClient to a test method, Get_EndpointsReturnSuccessAndCorrectContentType. The method verifies the response status code is successful (200-299) and the Content-Type header is text/html; charset=utf-8 for several app pages.
CreateClient() creates an instance of HttpClient that automatically follows redirects and handles cookies.
public class BasicTests
: IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public BasicTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
}
[Theory]
[InlineData("/")]
[InlineData("/Index")]
[InlineData("/About")]
[InlineData("/Privacy")]
[InlineData("/Contact")]
public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
{
// Arrange
var client = _factory.CreateClient();
// Act
var response = await client.GetAsync(url);
// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.Equal("text/html; charset=utf-8",
response.Content.Headers.ContentType.ToString());
}
}
By default, non-essential cookies aren't preserved across requests when the General Data Protection Regulation consent policy is enabled. To preserve non-essential cookies, such as those used by the TempData provider, mark them as essential in your tests. For instructions on marking a cookie as essential, see Essential cookies.
AngleSharp vs Application Parts for antiforgery checks
Web host configuration can be created independently of the test classes by inheriting from WebApplicationFactory<TEntryPoint> to create one or more custom factories:
public class CustomWebApplicationFactory<TProgram>
: WebApplicationFactory<TProgram> where TProgram : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
var dbContextDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbContextOptions<ApplicationDbContext>));
services.Remove(dbContextDescriptor);
var dbConnectionDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbConnection));
services.Remove(dbConnectionDescriptor);
// Create open SqliteConnection so EF won't automatically close it.
services.AddSingleton<DbConnection>(container =>
{
var connection = new SqliteConnection("DataSource=:memory:");
connection.Open();
return connection;
});
services.AddDbContext<ApplicationDbContext>((container, options) =>
{
var connection = container.GetRequiredService<DbConnection>();
options.UseSqlite(connection);
});
});
builder.UseEnvironment("Development");
}
}
The SUT's database context is registered in Program.cs. The test app's builder.ConfigureServices callback is executed after the app's Program.cs code is executed. To use a different database for the tests than the app's database, the app's database context must be replaced in builder.ConfigureServices.
The sample app finds the service descriptor for the database context and uses the descriptor to remove the service registration. The factory then adds a new ApplicationDbContext that uses an in-memory database for the tests..
To connect to a different database, change the DbConnection. To use a SQL Server test database:
The sample app's client is configured to prevent the HttpClient from following redirects. As explained later in the Mock authentication section, this permits tests to check the result of the app's first response. The first response is a redirect in many of these tests with a Location header.
A typical test uses the HttpClient and helper methods to process the request and the response:
[Fact]
public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot()
{
// Arrange
var defaultPage = await _client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
//Act
var response = await _client.SendAsync(
(IHtmlFormElement)content.QuerySelector("form[id='messages']"),
(IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']"));
// Assert
Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("/", response.Headers.Location.OriginalString);
}
Any POST request to the SUT must satisfy the antiforgery check that's automatically made by the app's data protection antiforgery system. In order to arrange for a test's POST request, the test app must:
Make a request for the page.
Parse the antiforgery cookie and request validation token from the response.
Make the POST request with the antiforgery cookie and request validation token in place.
The SendAsync helper extension methods (Helpers/HttpClientExtensions.cs) and the GetDocumentAsync helper method (Helpers/HtmlHelpers.cs) in the sample app use the AngleSharp parser to handle the antiforgery check with the following methods:
GetDocumentAsync: Receives the HttpResponseMessage and returns an IHtmlDocument. GetDocumentAsync uses a factory that prepares a virtual response based on the original HttpResponseMessage. For more information, see the AngleSharp documentation.
SendAsync extension methods for the HttpClient compose an HttpRequestMessage and call SendAsync(HttpRequestMessage) to submit requests to the SUT. Overloads for SendAsync accept the HTML form (IHtmlFormElement) and the following:
Submit button of the form (IHtmlElement)
Form values collection (IEnumerable<KeyValuePair<string, string>>)
Submit button (IHtmlElement) and form values (IEnumerable<KeyValuePair<string, string>>)
AngleSharp is a third-party parsing library used for demonstration purposes in this article and the sample app. AngleSharp isn't supported or required for integration testing of ASP.NET Core apps. Other parsers can be used, such as the Html Agility Pack (HAP). Another approach is to write code to handle the antiforgery system's request verification token and antiforgery cookie directly. See AngleSharp vs Application Parts for antiforgery checks in this article for more information.
When additional configuration is required within a test method, WithWebHostBuilder creates a new WebApplicationFactory with an IWebHostBuilder that is further customized by configuration.
The sample code calls WithWebHostBuilder to replace configured services with test stubs. For more information and example usage, see Inject mock services in this article.
The Post_DeleteMessageHandler_ReturnsRedirectToRoot test method of the sample app demonstrates the use of WithWebHostBuilder. This test performs a record delete in the database by triggering a form submission in the SUT.
Because another test in the IndexPageTests class performs an operation that deletes all of the records in the database and may run before the Post_DeleteMessageHandler_ReturnsRedirectToRoot method, the database is reseeded in this test method to ensure that a record is present for the SUT to delete. Selecting the first delete button of the messages form in the SUT is simulated in the request to the SUT:
[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
// Arrange
using (var scope = _factory.Services.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var db = scopedServices.GetRequiredService<ApplicationDbContext>();
Utilities.ReinitializeDbForTests(db);
}
var defaultPage = await _client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
//Act
var response = await _client.SendAsync(
(IHtmlFormElement)content.QuerySelector("form[id='messages']"),
(IHtmlButtonElement)content.QuerySelector("form[id='messages']")
.QuerySelector("div[class='panel-body']")
.QuerySelector("button"));
// Assert
Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("/", response.Headers.Location.OriginalString);
}
NOTE: To avoid HTTPS redirection warnings in logs when using HTTPS Redirection Middleware, set BaseAddress = new Uri("https://localhost")
Inject mock services
Services can be overridden in a test with a call to ConfigureTestServices on the host builder. To scope the overridden services to the test itself, the WithWebHostBuilder method is used to retrieve a host builder. This can be seen in the following tests:
The sample SUT includes a scoped service that returns a quote. The quote is embedded in a hidden field on the Index page when the Index page is requested.
Services/IQuoteService.cs:
public interface IQuoteService
{
Task<string> GenerateQuote();
}
The following markup is generated when the SUT app is run:
<input id="quote" type="hidden" value="Come on, Sarah. We've an appointment in
London, and we're already 30,000 years late.">
To test the service and quote injection in an integration test, a mock service is injected into the SUT by the test. The mock service replaces the app's QuoteService with a service provided by the test app, called TestQuoteService:
ConfigureTestServices is called, and the scoped service is registered:
[Fact]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddScoped<IQuoteService, TestQuoteService>();
});
})
.CreateClient();
//Act
var defaultPage = await client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
var quoteElement = content.QuerySelector("#quote");
// Assert
Assert.Equal("Something's interfering with time, Mr. Scarman, " +
"and time is my business.", quoteElement.Attributes["value"].Value);
}
The markup produced during the test's execution reflects the quote text supplied by TestQuoteService, thus the assertion passes:
<input id="quote" type="hidden" value="Something's interfering with time,
Mr. Scarman, and time is my business.">
Mock authentication
Tests in the AuthTests class check that a secure endpoint:
Redirects an unauthenticated user to the app's sign in page.
[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
// Arrange
var client = _factory.CreateClient(
new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
// Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.StartsWith("http://localhost/Identity/Account/Login",
response.Headers.Location.OriginalString);
}
By disallowing the client to follow the redirect, the following checks can be made:
The status code returned by the SUT can be checked against the expected HttpStatusCode.Redirect result, not the final status code after the redirect to the sign in page, which would be HttpStatusCode.OK.
The Location header value in the response headers is checked to confirm that it starts with http://localhost/Identity/Account/Login, not the final sign in page response, where the Location header wouldn't be present.
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder)
: base(options, logger, encoder)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "TestScheme");
var result = AuthenticateResult.Success(ticket);
return Task.FromResult(result);
}
}
The TestAuthHandler is called to authenticate a user when the authentication scheme is set to TestScheme where AddAuthentication is registered for ConfigureTestServices. It's important for the TestScheme scheme to match the scheme your app expects. Otherwise, authentication won't work.
For more information on WebApplicationFactoryClientOptions, see the Client options section.
Basic tests for authentication middleware
See this GitHub repository for basic tests of authentication middleware. It contains a test server that’s specific to the test scenario.
Set the environment
Set the environment in the custom application factory:
public class CustomWebApplicationFactory<TProgram>
: WebApplicationFactory<TProgram> where TProgram : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
var dbContextDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbContextOptions<ApplicationDbContext>));
services.Remove(dbContextDescriptor);
var dbConnectionDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbConnection));
services.Remove(dbConnectionDescriptor);
// Create open SqliteConnection so EF won't automatically close it.
services.AddSingleton<DbConnection>(container =>
{
var connection = new SqliteConnection("DataSource=:memory:");
connection.Open();
return connection;
});
services.AddDbContext<ApplicationDbContext>((container, options) =>
{
var connection = container.GetRequiredService<DbConnection>();
options.UseSqlite(connection);
});
});
builder.UseEnvironment("Development");
}
}
How the test infrastructure infers the app content root path
The WebApplicationFactory constructor infers the app content root path by searching for a WebApplicationFactoryContentRootAttribute on the assembly containing the integration tests with a key equal to the TEntryPoint assembly System.Reflection.Assembly.FullName. In case an attribute with the correct key isn't found, WebApplicationFactory falls back to searching for a solution file (.sln) and appends the TEntryPoint assembly name to the solution directory. The app root directory (the content root path) is used to discover views and content files.
Disable shadow copying
Shadow copying causes the tests to execute in a different directory than the output directory. If your tests rely on loading files relative to Assembly.Location and you encounter issues, you might have to disable shadow copying.
To disable shadow copying when using xUnit, create a xunit.runner.json file in your test project directory, with the correct configuration setting:
{
"shadowCopy": false
}
Disposal of objects
After the tests of the IClassFixture implementation are executed, TestServer and HttpClient are disposed when xUnit disposes of the WebApplicationFactory. If objects instantiated by the developer require disposal, dispose of them in the IClassFixture implementation. For more information, see Implementing a Dispose method.
Allows a user to add, delete one, delete all, and analyze messages.
Test app
tests/RazorPagesProject.Tests
Used to integration test the SUT.
The tests can be run using the built-in test features of an IDE, such as Visual Studio. If using Visual Studio Code or the command line, execute the following command at a command prompt in the tests/RazorPagesProject.Tests directory:
dotnet test
Message app (SUT) organization
The SUT is a Razor Pages message system with the following characteristics:
The Index page of the app (Pages/Index.cshtml and Pages/Index.cshtml.cs) provides a UI and page model methods to control the addition, deletion, and analysis of messages (average words per message).
A message is described by the Message class (Data/Message.cs) with two properties: Id (key) and Text (message). The Text property is required and limited to 200 characters.
The app contains a data access layer (DAL) in its database context class, AppDbContext (Data/AppDbContext.cs).
If the database is empty on app startup, the message store is initialized with three messages.
The app includes a /SecurePage that can only be accessed by an authenticated user.
†The EF article, Test with InMemory, explains how to use an in-memory database for tests with MSTest. This topic uses the xUnit test framework. Test concepts and test implementations across different test frameworks are similar but not identical.
Obtaining a GitHub user profile and checking the profile's user login.
BasicTests
Contains a test method for routing and content type.
IntegrationTests
Contains the integration tests for the Index page using custom WebApplicationFactory class.
Helpers/Utilities
Utilities.cs contains the InitializeDbForTests method used to seed the database with test data.
HtmlHelpers.cs provides a method to return an AngleSharp IHtmlDocument for use by the test methods.
HttpClientExtensions.cs provide overloads for SendAsync to submit requests to the SUT.
The test framework is xUnit. Integration tests are conducted using the Microsoft.AspNetCore.TestHost, which includes the TestServer. Because the Microsoft.AspNetCore.Mvc.Testing package is used to configure the test host and test server, the TestHost and TestServer packages don't require direct package references in the test app's project file or developer configuration in the test app.
Integration tests usually require a small dataset in the database prior to the test execution. For example, a delete test calls for a database record deletion, so the database must have at least one record for the delete request to succeed.
The sample app seeds the database with three messages in Utilities.cs that tests can use when they execute:
public static void InitializeDbForTests(ApplicationDbContext db)
{
db.Messages.AddRange(GetSeedingMessages());
db.SaveChanges();
}
public static void ReinitializeDbForTests(ApplicationDbContext db)
{
db.Messages.RemoveRange(db.Messages);
InitializeDbForTests(db);
}
public static List<Message> GetSeedingMessages()
{
return new List<Message>()
{
new Message(){ Text = "TEST RECORD: You're standing on my scarf." },
new Message(){ Text = "TEST RECORD: Would you like a jelly baby?" },
new Message(){ Text = "TEST RECORD: To the rational mind, " +
"nothing is inexplicable; only unexplained." }
};
}
The SUT's database context is registered in Program.cs. The test app's builder.ConfigureServices callback is executed after the app's Program.cs code is executed. To use a different database for the tests, the app's database context must be replaced in builder.ConfigureServices. For more information, see the Customize WebApplicationFactory section.
This article assumes a basic understanding of unit tests. If unfamiliar with test concepts, see the Unit Testing in .NET Core and .NET Standard article and its linked content.
The sample app is a Razor Pages app and assumes a basic understanding of Razor Pages. If you're unfamiliar with Razor Pages, see the following articles:
For testing SPAs, we recommend a tool such as Playwright for .NET, which can automate a browser.
Introduction to integration tests
Integration tests evaluate an app's components on a broader level than unit tests. Unit tests are used to test isolated software components, such as individual class methods. Integration tests confirm that two or more app components work together to produce an expected result, possibly including every component required to fully process a request.
These broader tests are used to test the app's infrastructure and whole framework, often including the following components:
Database
File system
Network appliances
Request-response pipeline
Unit tests use fabricated components, known as fakes or mock objects, in place of infrastructure components.
In contrast to unit tests, integration tests:
Use the actual components that the app uses in production.
Require more code and data processing.
Take longer to run.
Therefore, limit the use of integration tests to the most important infrastructure scenarios. If a behavior can be tested using either a unit test or an integration test, choose the unit test.
In discussions of integration tests, the tested project is frequently called the System Under Test, or "SUT" for short. "SUT" is used throughout this article to refer to the ASP.NET Core app being tested.
Don't write integration tests for every permutation of data and file access with databases and file systems. Regardless of how many places across an app interact with databases and file systems, a focused set of read, write, update, and delete integration tests are usually capable of adequately testing database and file system components. Use unit tests for routine tests of method logic that interact with these components. In unit tests, the use of infrastructure fakes or mocks result in faster test execution.
ASP.NET Core integration tests
Integration tests in ASP.NET Core require the following:
A test project is used to contain and execute the tests. The test project has a reference to the SUT.
The test project creates a test web host for the SUT and uses a test server client to handle requests and responses with the SUT.
A test runner is used to execute the tests and report the test results.
Integration tests follow a sequence of events that include the usual Arrange, Act, and Assert test steps:
The SUT's web host is configured.
A test server client is created to submit requests to the app.
The Arrange test step is executed: The test app prepares a request.
The Act test step is executed: The client submits the request and receives the response.
The Assert test step is executed: The actual response is validated as a pass or fail based on an expected response.
The process continues until all of the tests are executed.
The test results are reported.
Usually, the test web host is configured differently than the app's normal web host for the test runs. For example, a different database or different app settings might be used for the tests.
Infrastructure components, such as the test web host and in-memory test server (TestServer), are provided or managed by the Microsoft.AspNetCore.Mvc.Testing package. Use of this package streamlines test creation and execution.
The Microsoft.AspNetCore.Mvc.Testing package handles the following tasks:
Copies the dependencies file (.deps) from the SUT into the test project's bin directory.
Sets the content root to the SUT's project root so that static files and pages/views are found when the tests are executed.
Provides the WebApplicationFactory class to streamline bootstrapping the SUT with TestServer.
The unit tests documentation describes how to set up a test project and test runner, along with detailed instructions on how to run tests and recommendations for how to name tests and test classes.
Separate unit tests from integration tests into different projects. Separating the tests:
Helps ensure that infrastructure testing components aren't accidentally included in the unit tests.
Allows control over which set of tests are run.
There's virtually no difference between the configuration for tests of Razor Pages apps and MVC apps. The only difference is in how the tests are named. In a Razor Pages app, tests of page endpoints are usually named after the page model class (for example, IndexPageTests to test component integration for the Index page). In an MVC app, tests are usually organized by controller classes and named after the controllers they test (for example, HomeControllerTests to test component integration for the Home controller).
Specify the Web SDK in the project file (<Project Sdk="Microsoft.NET.Sdk.Web">).
These prerequisites can be seen in the sample app. Inspect the tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj file. The sample app uses the xUnit test framework and the AngleSharp parser library, so the sample app also references:
Test classes implement a class fixture interface (IClassFixture) to indicate the class contains tests and provide shared object instances across the tests in the class.
The following test class, BasicTests, uses the WebApplicationFactory to bootstrap the SUT and provide an HttpClient to a test method, Get_EndpointsReturnSuccessAndCorrectContentType. The method verifies the response status code is successful (200-299) and the Content-Type header is text/html; charset=utf-8 for several app pages.
CreateClient() creates an instance of HttpClient that automatically follows redirects and handles cookies.
public class BasicTests
: IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public BasicTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
}
[Theory]
[InlineData("/")]
[InlineData("/Index")]
[InlineData("/About")]
[InlineData("/Privacy")]
[InlineData("/Contact")]
public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
{
// Arrange
var client = _factory.CreateClient();
// Act
var response = await client.GetAsync(url);
// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.Equal("text/html; charset=utf-8",
response.Content.Headers.ContentType.ToString());
}
}
By default, non-essential cookies aren't preserved across requests when the General Data Protection Regulation consent policy is enabled. To preserve non-essential cookies, such as those used by the TempData provider, mark them as essential in your tests. For instructions on marking a cookie as essential, see Essential cookies.
AngleSharp vs Application Parts for antiforgery checks
Web host configuration can be created independently of the test classes by inheriting from WebApplicationFactory<TEntryPoint> to create one or more custom factories:
public class CustomWebApplicationFactory<TProgram>
: WebApplicationFactory<TProgram> where TProgram : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
var dbContextDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));
services.Remove(dbContextDescriptor);
var dbConnectionDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbConnection));
services.Remove(dbConnectionDescriptor);
// Create open SqliteConnection so EF won't automatically close it.
services.AddSingleton<DbConnection>(container =>
{
var connection = new SqliteConnection("DataSource=:memory:");
connection.Open();
return connection;
});
services.AddDbContext<ApplicationDbContext>((container, options) =>
{
var connection = container.GetRequiredService<DbConnection>();
options.UseSqlite(connection);
});
});
builder.UseEnvironment("Development");
}
}
The SUT's database context is registered in Program.cs. The test app's builder.ConfigureServices callback is executed after the app's Program.cs code is executed. To use a different database for the tests than the app's database, the app's database context must be replaced in builder.ConfigureServices.
The sample app finds the service descriptor for the database context and uses the descriptor to remove the service registration. The factory then adds a new ApplicationDbContext that uses an in-memory database for the tests..
To connect to a different database, change the DbConnection. To use a SQL Server test database:
The sample app's client is configured to prevent the HttpClient from following redirects. As explained later in the Mock authentication section, this permits tests to check the result of the app's first response. The first response is a redirect in many of these tests with a Location header.
A typical test uses the HttpClient and helper methods to process the request and the response:
[Fact]
public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot()
{
// Arrange
var defaultPage = await _client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
//Act
var response = await _client.SendAsync(
(IHtmlFormElement)content.QuerySelector("form[id='messages']"),
(IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']"));
// Assert
Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("/", response.Headers.Location.OriginalString);
}
Any POST request to the SUT must satisfy the antiforgery check that's automatically made by the app's data protection antiforgery system. In order to arrange for a test's POST request, the test app must:
Make a request for the page.
Parse the antiforgery cookie and request validation token from the response.
Make the POST request with the antiforgery cookie and request validation token in place.
The SendAsync helper extension methods (Helpers/HttpClientExtensions.cs) and the GetDocumentAsync helper method (Helpers/HtmlHelpers.cs) in the sample app use the AngleSharp parser to handle the antiforgery check with the following methods:
GetDocumentAsync: Receives the HttpResponseMessage and returns an IHtmlDocument. GetDocumentAsync uses a factory that prepares a virtual response based on the original HttpResponseMessage. For more information, see the AngleSharp documentation.
SendAsync extension methods for the HttpClient compose an HttpRequestMessage and call SendAsync(HttpRequestMessage) to submit requests to the SUT. Overloads for SendAsync accept the HTML form (IHtmlFormElement) and the following:
Submit button of the form (IHtmlElement)
Form values collection (IEnumerable<KeyValuePair<string, string>>)
Submit button (IHtmlElement) and form values (IEnumerable<KeyValuePair<string, string>>)
AngleSharp is a third-party parsing library used for demonstration purposes in this article and the sample app. AngleSharp isn't supported or required for integration testing of ASP.NET Core apps. Other parsers can be used, such as the Html Agility Pack (HAP). Another approach is to write code to handle the antiforgery system's request verification token and antiforgery cookie directly. See AngleSharp vs Application Parts for antiforgery checks in this article for more information.
When additional configuration is required within a test method, WithWebHostBuilder creates a new WebApplicationFactory with an IWebHostBuilder that is further customized by configuration.
The sample code calls WithWebHostBuilder to replace configured services with test stubs. For more information and example usage, see Inject mock services in this article.
The Post_DeleteMessageHandler_ReturnsRedirectToRoot test method of the sample app demonstrates the use of WithWebHostBuilder. This test performs a record delete in the database by triggering a form submission in the SUT.
Because another test in the IndexPageTests class performs an operation that deletes all of the records in the database and may run before the Post_DeleteMessageHandler_ReturnsRedirectToRoot method, the database is reseeded in this test method to ensure that a record is present for the SUT to delete. Selecting the first delete button of the messages form in the SUT is simulated in the request to the SUT:
[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
// Arrange
using (var scope = _factory.Services.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var db = scopedServices.GetRequiredService<ApplicationDbContext>();
Utilities.ReinitializeDbForTests(db);
}
var defaultPage = await _client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
//Act
var response = await _client.SendAsync(
(IHtmlFormElement)content.QuerySelector("form[id='messages']"),
(IHtmlButtonElement)content.QuerySelector("form[id='messages']")
.QuerySelector("div[class='panel-body']")
.QuerySelector("button"));
// Assert
Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("/", response.Headers.Location.OriginalString);
}
NOTE: To avoid HTTPS redirection warnings in logs when using HTTPS Redirection Middleware, set BaseAddress = new Uri("https://localhost")
Inject mock services
Services can be overridden in a test with a call to ConfigureTestServices on the host builder. To scope the overridden services to the test itself, the WithWebHostBuilder method is used to retrieve a host builder. This can be seen in the following tests:
The sample SUT includes a scoped service that returns a quote. The quote is embedded in a hidden field on the Index page when the Index page is requested.
Services/IQuoteService.cs:
public interface IQuoteService
{
Task<string> GenerateQuote();
}
The following markup is generated when the SUT app is run:
<input id="quote" type="hidden" value="Come on, Sarah. We've an appointment in
London, and we're already 30,000 years late.">
To test the service and quote injection in an integration test, a mock service is injected into the SUT by the test. The mock service replaces the app's QuoteService with a service provided by the test app, called TestQuoteService:
ConfigureTestServices is called, and the scoped service is registered:
[Fact]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddScoped<IQuoteService, TestQuoteService>();
});
})
.CreateClient();
//Act
var defaultPage = await client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
var quoteElement = content.QuerySelector("#quote");
// Assert
Assert.Equal("Something's interfering with time, Mr. Scarman, " +
"and time is my business.", quoteElement.Attributes["value"].Value);
}
The markup produced during the test's execution reflects the quote text supplied by TestQuoteService, thus the assertion passes:
<input id="quote" type="hidden" value="Something's interfering with time,
Mr. Scarman, and time is my business.">
Mock authentication
Tests in the AuthTests class check that a secure endpoint:
Redirects an unauthenticated user to the app's sign in page.
[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
// Arrange
var client = _factory.CreateClient(
new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
// Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.StartsWith("http://localhost/Identity/Account/Login",
response.Headers.Location.OriginalString);
}
By disallowing the client to follow the redirect, the following checks can be made:
The status code returned by the SUT can be checked against the expected HttpStatusCode.Redirect result, not the final status code after the redirect to the sign in page, which would be HttpStatusCode.OK.
The Location header value in the response headers is checked to confirm that it starts with http://localhost/Identity/Account/Login, not the final sign in page response, where the Location header wouldn't be present.
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder)
: base(options, logger, encoder)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "TestScheme");
var result = AuthenticateResult.Success(ticket);
return Task.FromResult(result);
}
}
The TestAuthHandler is called to authenticate a user when the authentication scheme is set to TestScheme where AddAuthentication is registered for ConfigureTestServices. It's important for the TestScheme scheme to match the scheme your app expects. Otherwise, authentication won't work.
For more information on WebApplicationFactoryClientOptions, see the Client options section.
Basic tests for authentication middleware
See this GitHub repository for basic tests of authentication middleware. It contains a test server that’s specific to the test scenario.
Set the environment
Set the environment in the custom application factory:
public class CustomWebApplicationFactory<TProgram>
: WebApplicationFactory<TProgram> where TProgram : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
var dbContextDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));
services.Remove(dbContextDescriptor);
var dbConnectionDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbConnection));
services.Remove(dbConnectionDescriptor);
// Create open SqliteConnection so EF won't automatically close it.
services.AddSingleton<DbConnection>(container =>
{
var connection = new SqliteConnection("DataSource=:memory:");
connection.Open();
return connection;
});
services.AddDbContext<ApplicationDbContext>((container, options) =>
{
var connection = container.GetRequiredService<DbConnection>();
options.UseSqlite(connection);
});
});
builder.UseEnvironment("Development");
}
}
How the test infrastructure infers the app content root path
The WebApplicationFactory constructor infers the app content root path by searching for a WebApplicationFactoryContentRootAttribute on the assembly containing the integration tests with a key equal to the TEntryPoint assembly System.Reflection.Assembly.FullName. In case an attribute with the correct key isn't found, WebApplicationFactory falls back to searching for a solution file (.sln) and appends the TEntryPoint assembly name to the solution directory. The app root directory (the content root path) is used to discover views and content files.
Disable shadow copying
Shadow copying causes the tests to execute in a different directory than the output directory. If your tests rely on loading files relative to Assembly.Location and you encounter issues, you might have to disable shadow copying.
To disable shadow copying when using xUnit, create a xunit.runner.json file in your test project directory, with the correct configuration setting:
{
"shadowCopy": false
}
Disposal of objects
After the tests of the IClassFixture implementation are executed, TestServer and HttpClient are disposed when xUnit disposes of the WebApplicationFactory. If objects instantiated by the developer require disposal, dispose of them in the IClassFixture implementation. For more information, see Implementing a Dispose method.
Allows a user to add, delete one, delete all, and analyze messages.
Test app
tests/RazorPagesProject.Tests
Used to integration test the SUT.
The tests can be run using the built-in test features of an IDE, such as Visual Studio. If using Visual Studio Code or the command line, execute the following command at a command prompt in the tests/RazorPagesProject.Tests directory:
dotnet test
Message app (SUT) organization
The SUT is a Razor Pages message system with the following characteristics:
The Index page of the app (Pages/Index.cshtml and Pages/Index.cshtml.cs) provides a UI and page model methods to control the addition, deletion, and analysis of messages (average words per message).
A message is described by the Message class (Data/Message.cs) with two properties: Id (key) and Text (message). The Text property is required and limited to 200 characters.
The app contains a data access layer (DAL) in its database context class, AppDbContext (Data/AppDbContext.cs).
If the database is empty on app startup, the message store is initialized with three messages.
The app includes a /SecurePage that can only be accessed by an authenticated user.
†The EF article, Test with InMemory, explains how to use an in-memory database for tests with MSTest. This topic uses the xUnit test framework. Test concepts and test implementations across different test frameworks are similar but not identical.
Obtaining a GitHub user profile and checking the profile's user login.
BasicTests
Contains a test method for routing and content type.
IntegrationTests
Contains the integration tests for the Index page using custom WebApplicationFactory class.
Helpers/Utilities
Utilities.cs contains the InitializeDbForTests method used to seed the database with test data.
HtmlHelpers.cs provides a method to return an AngleSharp IHtmlDocument for use by the test methods.
HttpClientExtensions.cs provide overloads for SendAsync to submit requests to the SUT.
The test framework is xUnit. Integration tests are conducted using the Microsoft.AspNetCore.TestHost, which includes the TestServer. Because the Microsoft.AspNetCore.Mvc.Testing package is used to configure the test host and test server, the TestHost and TestServer packages don't require direct package references in the test app's project file or developer configuration in the test app.
Integration tests usually require a small dataset in the database prior to the test execution. For example, a delete test calls for a database record deletion, so the database must have at least one record for the delete request to succeed.
The sample app seeds the database with three messages in Utilities.cs that tests can use when they execute:
public static void InitializeDbForTests(ApplicationDbContext db)
{
db.Messages.AddRange(GetSeedingMessages());
db.SaveChanges();
}
public static void ReinitializeDbForTests(ApplicationDbContext db)
{
db.Messages.RemoveRange(db.Messages);
InitializeDbForTests(db);
}
public static List<Message> GetSeedingMessages()
{
return new List<Message>()
{
new Message(){ Text = "TEST RECORD: You're standing on my scarf." },
new Message(){ Text = "TEST RECORD: Would you like a jelly baby?" },
new Message(){ Text = "TEST RECORD: To the rational mind, " +
"nothing is inexplicable; only unexplained." }
};
}
The SUT's database context is registered in Program.cs. The test app's builder.ConfigureServices callback is executed after the app's Program.cs code is executed. To use a different database for the tests, the app's database context must be replaced in builder.ConfigureServices. For more information, see the Customize WebApplicationFactory section.
The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see our contributor guide.
ASP.NET Core
feedback
ASP.NET Core
is an open source project. Select a link to provide feedback:
This topic describes some specific techniques for unit testing controllers in Web API 2. Before reading this topic, you might want to read the tutorial Unit...
Start testing your C# apps by using the testing tools in Visual Studio. Learn to write tests, use Test Explorer, create test suites, and apply the red, green, refactor pattern to write code.