Appearance
Are you an LLM? You can read better optimized documentation at /testing/integration_testing.md for this page in Markdown format
Integration testing
This document provides a technical overview of the integration testing strategy for TheExampleApp. Integration tests are designed to verify that different parts of the application work together as expected. They operate by making in-memory HTTP requests to a test server, exercising the full application pipeline from routing and middleware to page rendering and service logic.
For a broader perspective on testing, refer to the Testing Overview.
Integration test approach
Our integration testing approach for this ASP.NET Core 2.1 application is centered around the Microsoft.AspNetCore.TestHost library. This powerful tool allows us to host the application entirely in-memory, eliminating the need for a separate web server process or network overhead.
Key characteristics of this approach include:
- End-to-End Pipeline Testing: Each test initiates a real HTTP request that travels through the application's complete request pipeline as defined in
Startup.cs. This includes all configured middleware (e.g., static files, session, localization, routing), dependency injection, and final page execution. - In-Memory Hosting: The
TestServerclass creates an in-memory host for the web application. AnHttpClientis then used to dispatch requests to this server. This is significantly faster and more reliable than tests that rely on an externaldotnet runprocess. - Real Dependencies: By default, these tests use the application's actual service registrations, including the
IContentfulClient. This means tests will make live HTTP calls to the configured Contentful CMS, verifying the integration with this critical external service. - Test Framework: Tests are written and executed using the xUnit testing framework, as indicated by the
[Fact]attributes and the project dependencies inTheExampleApp.IntegrationTests.csproj.
The integration test project (TheExampleApp.IntegrationTests) references the main web application project (TheExampleApp) to gain access to its Startup class and other internal components.
xml
<!-- TheExampleApp.IntegrationTests/TheExampleApp.IntegrationTests.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<!-- Core package for in-memory test hosting -->
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="2.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0" />
<PackageReference Include="xunit" Version="2.3.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
</ItemGroup>
<ItemGroup>
<!-- Reference to the main application project -->
<ProjectReference Include="..\\TheExampleApp\\TheExampleApp.csproj" />
</ItemGroup>
</Project>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
PageTests implementation
The primary implementation of our integration tests can be found in the PageTests.cs class. This class demonstrates the setup and execution of tests against various application pages.
WebApplicationFactory and TestServer Usage
In modern ASP.NET Core, WebApplicationFactory is the standard for bootstrapping integration tests. However, in this project's .NET Core 2.1 context, the setup is done manually using WebHostBuilder and TestServer, which WebApplicationFactory would otherwise abstract away.
The constructor of PageTests is responsible for configuring and starting the test server for each test run.
csharp
// TheExampleApp.IntegrationTests/PageTests.cs
public class PageTests
{
private readonly TestServer _server;
private readonly HttpClient _client;
public PageTests()
{
// Arrange: Set up the test server
var startupAssembly = typeof(Startup).GetTypeInfo().Assembly;
// Locate the content root of the main web project to find views, wwwroot, and appsettings.json
var contentRoot = GetProjectPath("", startupAssembly);
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 from the web app
_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
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
Key steps in the setup:
GetProjectPath: A crucial helper method that navigates the directory structure to find the root of theTheExampleAppproject. This is necessary so theTestServercan locate Razor pages, static assets, and configuration files.WebHostBuilder: This builder configures the test host..UseContentRoot(): Sets the application's base path..UseEnvironment("Development"): Forces the application to run in theDevelopmentenvironment, which may enable features like the developer exception page..UseStartup(typeof(Startup)): This is the most important part. It tells the test host to use the realStartup.csfrom the main application, ensuring that the test environment mirrors production as closely as possible regarding service registration and middleware configuration. See Service Configuration for more details.
TestServerandHttpClient: An instance ofTestServeris created from the builder, andCreateClient()provides anHttpClientpre-configured to send requests to the in-memory server.
Test scenarios
The PageTests.cs file covers several critical user journeys and edge cases across the following pages:
- Index page (
/) - Home page with English and German localization - Courses page (
/courses) - Course listing with localization - Individual course pages (
/courses/hello-contentful) - Course detail pages with localization - Lesson pages (
/courses/hello-contentful/lessons/content-model) - Individual lesson content with localization - Imprint page (
/Imprint) - Legal/company information with localization - Settings page (
/Settings) - Application configuration with form validation and API switching
Additionally, the tests verify proper HTTP status codes for non-existent resources (404 errors).
Page Load Tests
These tests verify that key pages render successfully and contain expected content. They serve as a baseline check for routing, data fetching from Contentful, and Razor page rendering.
A typical page load test makes a GET request and asserts two things:
- The HTTP status code is
200 OK. - The HTML response body contains specific, expected text.
csharp
// TheExampleApp.IntegrationTests/PageTests.cs
[Fact]
public async Task CoursesShouldReturn200()
{
// Act: Request the /courses page
var response = await _client.GetAsync("/courses");
var responseString = await response.Content.ReadAsStringAsync();
// Assert: Check for success status and specific content
response.EnsureSuccessStatusCode(); // Throws if not a 2xx status
Assert.Contains("<h1>All courses", responseString);
}
[Fact]
public async Task CoursesShouldReturn200ForGerman()
{
// Act: Request the page with the German locale query parameter
var response = await _client.GetAsync("/courses?locale=de-DE");
var responseString = await response.Content.ReadAsStringAsync();
// Assert: Check for German-specific content
response.EnsureSuccessStatusCode();
Assert.Contains("<h1>Alle Kurse", responseString);
}
[Fact]
public async Task MissingCourseShouldReturn404()
{
// Act: Request a URL that should not resolve to a course
var response = await _client.GetAsync("/courses/no-such-thing");
// Assert: Verify the status code is 404 Not Found
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}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
30
31
32
33
34
35
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
30
31
32
33
34
35
These tests effectively validate the custom routing conventions for Razor Pages defined in Startup.cs.
Form Submission
Testing form submissions is more complex due to the need to handle ASP.NET Core's anti-forgery token mechanism. The PageTests class includes a helper method, GetRequestContentAsync, specifically for this purpose.
Anti-Forgery Token Handling Pattern:
- A
GETrequest is first sent to the page containing the form. - The
Set-Cookieheader from the response, which contains the anti-forgery cookie, is captured and added to theHttpClient's default headers for subsequent requests. - The HTML response body is parsed with a regular expression to extract the value of the hidden
__RequestVerificationTokenform field. - A
FormUrlEncodedContentobject is created containing the desired form data plus the extracted__RequestVerificationToken. - This content is then
POSTed to the appropriate handler endpoint.
csharp
// TheExampleApp.IntegrationTests/PageTests.cs
private static async Task<FormUrlEncodedContent> GetRequestContentAsync(HttpClient _client, string path, IDictionary<string, string> data)
{
// 1. Make a request for the resource.
var getResponse = await _client.GetAsync(path);
// 2. Set the response's antiforgery cookie on the HttpClient.
_client.DefaultRequestHeaders.Add("Cookie", getResponse.Headers.GetValues("Set-Cookie"));
// 3. Obtain the request verification token from the response.
var responseMarkup = await getResponse.Content.ReadAsStringAsync();
var regExp_RequestVerificationToken = new Regex("<input name=\\\"__RequestVerificationToken\\\" type=\\\"hidden\\\" value=\\\"(.*?)\\\" \\\\/>", RegexOptions.Compiled);
var token = regExp_RequestVerificationToken.Matches(responseMarkup)?.FirstOrDefault().Groups[1].Value;
// 4. Add the token to the form data for the request.
data.Add("__RequestVerificationToken", token);
return new FormUrlEncodedContent(data);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
This pattern is used to test both invalid and valid form submissions, such as updating application settings.
csharp
// TheExampleApp.IntegrationTests/PageTests.cs
[Fact]
public async Task PostingInvalidOptionsShouldReturn200WithErrorMessage()
{
// Arrange: Prepare form data with empty (invalid) values
var formContent = await GetRequestContentAsync(_client, "/Settings", new Dictionary<string, string>
{
{ "AppOptions.SpaceId", "" } ,
{ "AppOptions.AccessToken", "" },
{ "AppOptions.PreviewToken", "" },
});
// Act: Post the invalid data
var response = await _client.PostAsync("/Settings", formContent);
var responseString = await response.Content.ReadAsStringAsync();
// Assert: The page re-renders with validation error messages
response.EnsureSuccessStatusCode();
Assert.Contains(@"<span class=""field-validation-error"" data-valmsg-for=""AppOptions.SpaceId""", responseString);
}
[Fact]
public async Task PostingCorrectOptionsShouldReturn302()
{
// Arrange: Prepare form data with valid values
var formContent = await GetRequestContentAsync(_client, "/Settings", new Dictionary<string, string>
{
{ "AppOptions.SpaceId", "qz0n5cdakyl9" } ,
// ... other valid data
});
// Act: Post the valid data
var response = await _client.PostAsync("/Settings", formContent);
// Assert: A successful post should result in a redirect (Post-Redirect-Get pattern)
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
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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
30
31
32
33
34
35
36
Localization Testing
The application supports multiple locales (English and German). Integration tests verify that pages render correctly for different locales by passing the locale query parameter:
csharp
// TheExampleApp.IntegrationTests/PageTests.cs
[Fact]
public async Task IndexShouldReturn200ForGerman()
{
// Act: Request the home page with German locale
var response = await _client.GetAsync("/?locale=de-DE");
var responseString = await response.Content.ReadAsStringAsync();
// Assert: Check for German-specific content
response.EnsureSuccessStatusCode();
Assert.Contains("Dies ist die Beispielanwendung", responseString);
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
Similar localization tests exist for other pages including courses, lessons, imprint, and settings pages.
Additional Test Scenarios
Beyond basic page load and form submission tests, PageTests.cs includes several other important test scenarios:
API Switching: Tests verify that users can switch between the Content Delivery API (CDA) and Content Preview API (CPA):
csharp
[Fact]
public async Task PostingToSwitchApiShouldSwitchAPIAndReturn302()
{
var formContent = await GetRequestContentAsync(_client, "/Settings", new Dictionary<string, string>
{
{ "api", "cpa" } ,
{ "prevPage", "/" },
});
var response = await _client.PostAsync("/Settings?handler=SwitchApi", formContent);
Assert.Equal(HttpStatusCode.Found, response.StatusCode);
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
Editorial Features Toggle: Tests ensure that editorial features can be enabled via query parameters and persist in the form state:
csharp
[Fact]
public async Task SettingsShouldTurnOnEditorialFeaturesAndReturn200()
{
var response = await _client.GetAsync("/Settings?editorial_features=enabled");
var responseString = await response.Content.ReadAsStringAsync();
response.EnsureSuccessStatusCode();
Assert.Contains(@"<input id=""input-editorial-features"" type=""checkbox"" checked=""checked""", responseString);
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
Invalid Credentials Validation: Tests verify that invalid Contentful credentials produce appropriate error messages:
csharp
[Fact]
public async Task PostingBogusOptionsShouldReturn200WithErrorMessage()
{
var formContent = await GetRequestContentAsync(_client, "/Settings", new Dictionary<string, string>
{
{ "AppOptions.SpaceId", "321" } ,
{ "AppOptions.AccessToken", "421" },
{ "AppOptions.PreviewToken", "34" },
});
var response = await _client.PostAsync("/Settings", formContent);
var responseString = await response.Content.ReadAsStringAsync();
response.EnsureSuccessStatusCode();
Assert.Contains(@"This space does not exist or your access token is not associated with your space.", responseString);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Configuration
Test Server Setup
The core of the test configuration resides in the PageTests constructor and the GetProjectPath helper method. This setup ensures the test server runs with the correct application context.
csharp
// TheExampleApp.IntegrationTests/PageTests.cs
// Get the full path to the target project for testing
private static string GetProjectPath(string projectRelativePath, Assembly startupAssembly)
{
var projectName = startupAssembly.GetName().Name;
var applicationBasePath = System.AppContext.BaseDirectory;
var directoryInfo = new DirectoryInfo(applicationBasePath);
do
{
directoryInfo = directoryInfo.Parent;
var projectDirectoryInfo = new DirectoryInfo(Path.Combine(directoryInfo.FullName, projectRelativePath));
if (projectDirectoryInfo.Exists)
{
var projectFileInfo = new FileInfo(Path.Combine(projectDirectoryInfo.FullName, projectName, $"{projectName}.csproj"));
if (projectFileInfo.Exists)
{
return Path.Combine(projectDirectoryInfo.FullName, projectName);
}
}
}
while (directoryInfo.Parent != null);
throw new Exception($"Project root could not be located using the application root {applicationBasePath}.");
}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
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
This helper is robust but sensitive to the solution's folder structure. If the relative path between the test project and the main project changes, this code may need adjustment.
Test-Specific Configuration
The WebHostBuilder chain allows for overriding or augmenting the application's services for a test context. The InitializeServices method is provided for this purpose.
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
In its current form, this method ensures that the test host's ApplicationPartManager is aware of the controllers and view components in the main TheExampleApp assembly. This can be a necessary fix in some .NET Core testing scenarios where the test runner fails to discover these parts automatically.
This InitializeServices method is also the ideal place to mock dependencies. For example, to avoid hitting the live Contentful API and instead use mock data, one could remove the real IContentfulClient registration and add a mock implementation.
Note: While the current tests perform true integration testing against the live Contentful API (when configured), you can adapt this pattern for hermetic integration tests by mocking external services. This can lead to faster, more reliable tests that are independent of network or external service availability. This approach is closer to the philosophy of Unit Testing but applied at the integration level.