Appearance
Are you an LLM? You can read better optimized documentation at /testing/integration_tests.md for this page in Markdown format
Integration tests
Overview
Note: Based on the current project structure, there is no dedicated integration test project (
TheExampleApp.IntegrationTests) in the codebase. The test projectTheExampleApp.Testsis configured for unit testing with xUnit and Moq, and does not include theMicrosoft.AspNetCore.TestHostpackage required for integration testing. This document describes the recommended integration testing strategy forTheExampleAppshould such tests be implemented in the future.
This document explains the integration testing strategy for TheExampleApp. Integration tests are designed to verify that different parts of the application work together correctly, from the initial HTTP request through the entire ASP.NET Core pipeline to the final rendered HTML response. This provides a high level of confidence that our pages and features are functioning as expected from a user's perspective.
Integration testing approach
Our integration testing philosophy is to test the application "from the outside-in." Unlike unit tests, which test individual classes or methods in isolation, integration tests treat the application as a black box.
The core approach involves:
- Hosting the application in-memory: We use an in-memory test server to host the entire web application.
- Simulating HTTP requests: A test client sends HTTP
GETandPOSTrequests to specific application endpoints (e.g.,/,/courses,/settings). - Asserting on HTTP responses: The tests inspect the
HttpResponseMessageto verify:- Status Codes: Ensuring a
200 OKfor success,404 Not Foundfor missing pages, or302 Foundfor redirects. - Response Content: Analyzing the rendered HTML to confirm that the correct data, text, and UI elements are present. This is particularly important for a server-rendered application using Razor Pages.
- Status Codes: Ensuring a
This strategy validates the complete request-response cycle, including routing, middleware, dependency injection, controller/page model logic, and view rendering.
TestServer
The foundation of our integration tests is the Microsoft.AspNetCore.TestHost.TestServer. This component from the Microsoft.AspNetCore.TestHost package allows us to host the application entirely in-memory, without needing to deploy to a real web server or open network ports. This makes the tests fast, reliable, and self-contained.
The TestServer is configured and instantiated in the constructor of our main test class, PageTests.cs.
csharp
// TheExampleApp.IntegrationTests/PageTests.cs
public class PageTests
{
private readonly TestServer _server;
private readonly HttpClient _client;
public PageTests()
{
// Arrange
var startupAssembly = typeof(Startup).GetTypeInfo().Assembly;
// Locate the path to the main web application project
var contentRoot = GetProjectPath("", startupAssembly);
// Configure a WebHostBuilder that mirrors the real application's setup
var builder = new WebHostBuilder()
.UseContentRoot(contentRoot)
.UseEnvironment("Development")
.ConfigureAppConfiguration((b, i) => { i.AddJsonFile("appsettings.json"); })
.ConfigureServices(InitializeServices)
.UseStartup(typeof(Startup)); // Use the actual Startup class
_server = new TestServer(builder);
_client = _server.CreateClient();
}
// ... tests ...
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Key aspects of this setup:
WebHostBuilder: We construct aWebHostBuilderto configure the test host.UseContentRoot: This is critical for ensuring that static files and views (.cshtml) can be found correctly during test execution. TheGetProjectPathhelper method dynamically locates the main web project's directory.UseStartup(typeof(Startup)): This is the most important part. We instruct theTestServerto use the exact sameStartupclass as the production application. This means our tests run with the same services, configuration, and middleware pipeline, providing a highly accurate test environment.HttpClient: The_server.CreateClient()method returns anHttpClientthat is pre-configured to route requests directly to the in-memoryTestServer.
PageTests
The TheExampleApp.IntegrationTests/PageTests.cs file contains the suite of integration tests. These tests are written using the xUnit framework and cover various scenarios.
Basic Page Rendering and Content Verification
The simplest tests verify that a page loads successfully and contains expected content.
csharp
// TheExampleApp.IntegrationTests/PageTests.cs
[Fact]
public async Task CoursesShouldReturn200()
{
// Act: Send a GET request to the /courses endpoint
var response = await _client.GetAsync("/courses");
var responseString = await response.Content.ReadAsStringAsync();
// Assert
response.EnsureSuccessStatusCode(); // Throws if status code is not 2xx
Assert.Contains("Contentful Example App", responseString); // Check for layout content
Assert.Contains("<h1>All courses", responseString); // Check for page-specific content
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
Testing Internationalization (i18n)
The application supports multiple locales. Tests verify this by passing the locale query parameter and asserting that the response contains the correctly translated strings.
csharp
// TheExampleApp.IntegrationTests/PageTests.cs
[Fact]
public async Task CoursesShouldReturn200ForGerman()
{
// Act: Request the page with the German locale
var response = await _client.GetAsync("/courses?locale=de-DE");
var responseString = await response.Content.ReadAsStringAsync();
// Assert
response.EnsureSuccessStatusCode();
Assert.Contains("Dies ist die Beispielanwendung", responseString); // German layout text
Assert.Contains("<h1>Alle Kurse", responseString); // German page title
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
Testing Form Submissions and Anti-Forgery Tokens
Testing POST requests in ASP.NET Core requires handling the anti-forgery token. Our test suite includes a helper method, GetRequestContentAsync, to automate this process.
The process is:
- Send a
GETrequest to the page with the form. - Extract the anti-forgery cookie from the
Set-Cookieresponse header and add it to theHttpClient's default headers for subsequent requests. - Parse the HTML response to find the hidden
__RequestVerificationTokeninput field and extract its value. - Construct a
FormUrlEncodedContentobject containing the form data and the extracted token. POSTthis content to the form's handler.
csharp
// TheExampleApp.IntegrationTests/PageTests.cs
[Fact]
public async Task PostingCorrectOptionsShouldReturn302()
{
// Arrange: Use the helper to prepare form data with a valid anti-forgery token
var formContent = await GetRequestContentAsync(_client, "/Settings", new Dictionary<string, string>
{
{ "AppOptions.SpaceId", "qz0n5cdakyl9" } ,
{ "AppOptions.AccessToken", "df2a18b8a5b4426741408fc95fa4331c7388d502318c44a5b22b167c3c1b1d03" },
{ "AppOptions.PreviewToken", "10145c6d864960fdca694014ae5e7bdaa7de514a1b5d7fd8bd24027f90c49bbc" },
});
// Act: Post the form
var response = await _client.PostAsync("/Settings", formContent);
// Assert: A successful form post on the settings page should result in a redirect
Assert.Equal(HttpStatusCode.Found, response.StatusCode);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
This pattern is also used to test form validation errors by posting invalid data and asserting that the response contains the expected validation error messages.
Test Setup and Dependencies
All test setup is performed within the PageTests class constructor. This approach, while not using xUnit's IClassFixture, is effective for creating a new, clean instance of the application for each test run.
The InitializeServices method provides a hook to modify the application's dependency injection container for testing purposes.
csharp
// TheExampleApp.IntegrationTests/PageTests.cs
protected virtual void InitializeServices(IServiceCollection services)
{
var startupAssembly = typeof(Startup).GetTypeInfo().Assembly;
var manager = new ApplicationPartManager();
manager.ApplicationParts.Add(new AssemblyPart(startupAssembly));
manager.FeatureProviders.Add(new ControllerFeatureProvider());
manager.FeatureProviders.Add(new ViewComponentFeatureProvider());
services.AddSingleton(manager);
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
Currently, this method ensures that the test host correctly discovers controllers and view components from the main application assembly. It can be extended to register mock services or test-specific configurations.
Testing with real/mock Contentful
The current integration tests connect to a real Contentful space using the credentials from appsettings.json or those submitted in form posts (e.g., PostingCorrectOptionsShouldReturn302).
- Advantages: This provides the highest fidelity test, ensuring that our application correctly integrates with the live Contentful API, including any changes to the content models.
- Disadvantages:
- Brittleness: Tests can fail due to network issues, Contentful API downtime, or changes to the content in the test space.
- Speed: Network requests to an external service are significantly slower than in-memory operations.
- Dependency: Requires valid, and potentially secret, API credentials to be available in the test environment.
Strategy for Mocking Contentful
For more stable, faster, and isolated integration tests, the IContentfulClient can be replaced with a mock using a mocking framework like Moq (version 4.7.145, which is already available in the TheExampleApp.Tests project).
This would involve modifying the test setup to replace the default registration:
csharp
// Hypothetical example of mocking IContentfulClient using Moq
public PageTests()
{
// ...
var builder = new WebHostBuilder()
.UseContentRoot(contentRoot)
.UseEnvironment("Development")
.ConfigureAppConfiguration((b, i) => { i.AddJsonFile("appsettings.json"); })
// Use ConfigureTestServices for overrides
.ConfigureTestServices(services =>
{
// Create a mock Contentful client using Moq
var mockContentfulClient = new Mock<IContentfulClient>();
// Setup mock behavior, e.g., for fetching a course
var fakeCourse = new Course { Slug = "hello-contentful", Title = "Hello Contentful" };
mockContentfulClient.Setup(c => c.GetEntries<Course>(
It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new ContentfulCollection<Course> { Items = new[] { fakeCourse } });
// Replace the real IContentfulClient with the mock
services.AddSingleton<IContentfulClient>(mockContentfulClient.Object);
})
.UseStartup(typeof(Startup));
_server = new TestServer(builder);
_client = _server.CreateClient();
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Note:
ConfigureTestServicesis the recommended way to override services for integration tests in ASP.NET Core. This approach would allow us to create deterministic tests for scenarios like "content not found" or API errors without making any external network calls.
Running integration tests
The integration tests are standard xUnit tests and can be run like any other test in the solution.
- Visual Studio: Open the Test Explorer window (
Test > Test Explorer) and run the tests from theTheExampleApp.IntegrationTestsproject. - Command Line: Navigate to the integration test project directory and run the
dotnet testcommand.
bash
cd TheExampleApp.IntegrationTests
dotnet test1
2
2
Debugging integration tests
When an integration test fails, it can be due to an issue anywhere in the application stack. Here are some common issues and debugging strategies:
- File Not Found (Views/Static Files): If you see errors about views not being found, ensure the
GetProjectPathhelper is correctly locating the mainTheExampleAppproject directory. TheUseContentRootcall in theTestServersetup is essential. - Anti-Forgery Token Errors: If
POSTrequests are failing with a400 Bad Request, it's almost certainly an anti-forgery token issue. Ensure you are using theGetRequestContentAsyncpattern to correctly capture and resubmit the token. - Configuration Issues: The tests load
appsettings.json. If a test depends on specific configuration values, ensure they are present. For test-specific overrides, consider usingappsettings.Development.jsonor another configuration source in theWebHostBuilder. - Tracing the Request: The best way to debug a failing test is to trace the request's lifecycle. You can place breakpoints directly inside:
- The Razor Page's
OnGetorOnPostmethods. - Custom middleware in
Startup.cs. - Service constructors or methods that are called during the request.
- The Razor Page's
Since the test runs in-process, you can set a breakpoint in the test method itself and step through the _client.GetAsync() call directly into the application code.