In this module, we will cover integration testing using Aspire.Hosting.Testing with MSTest. Integration testing is crucial for ensuring that different parts of your application work together as expected. We will create a separate test project to test both the API and the web application.
Unit testing focuses on testing individual components or units of code in isolation. It ensures that each unit functions correctly on its own. In contrast, integration testing verifies that different components of the application work together as expected. It tests the interactions between various parts of the system, such as APIs, databases, and web applications.
In the context of distributed applications with Aspire, integration testing is essential to ensure that the different services and components communicate and function correctly together.
- Create a new test project named
IntegrationTests. - Add references to the required packages in the
IntegrationTests.csprojfile:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<PropertyGroup>
<EnableMSTestRunner>true</EnableMSTestRunner>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting.Testing" Version="13.1.0" />
<PackageReference Include="MSTest" Version="3.10.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AppHost\AppHost.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="System.Net" />
<Using Include="Microsoft.Extensions.DependencyInjection" />
<Using Include="Aspire.Hosting.ApplicationModel" />
<Using Include="Aspire.Hosting.Testing" />
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting" />
</ItemGroup>
</Project>This project file is fairly standard for a test project. The key elements are:
- A
PackageReferenceto the Aspire.Hosting.Testing NuGet package, which provides the necessary types and APIs for testing Aspire applications. - A
ProjectReferenceto the AppHost project, which gives the test project access to the target distributed application definition. - The
EnableMSTestRunnerandOutputTypesettings to configure the test project to run with the native MSTest runner.
Note: Any MSTest 3.x version is fine for this workshop. If your environment provides a newer 3.x, you can use that.
- Create test classes for integration tests:
The IntegrationTests.cs file tests the API and web application functionality:
namespace MyWeatherHub.Tests;
[TestClass]
public class IntegrationTests
{
[TestMethod]
public async Task TestApiGetZones()
{
// Arrange
var appHost = await DistributedApplicationTestingBuilder
.CreateAsync<Projects.AppHost>();
appHost.Services.ConfigureHttpClientDefaults(clientBuilder =>
{
clientBuilder.AddStandardResilienceHandler();
});
await using var app = await appHost.BuildAsync();
var resourceNotificationService = app.Services
.GetRequiredService<ResourceNotificationService>();
await app.StartAsync();
// Act
var httpClient = app.CreateHttpClient("api");
await resourceNotificationService.WaitForResourceAsync(
"api",
KnownResourceStates.Running
)
.WaitAsync(TimeSpan.FromSeconds(30));
var response = await httpClient.GetAsync("/zones");
// Assert
response.EnsureSuccessStatusCode();
var zones = await response.Content.ReadFromJsonAsync<Zone[]>();
Assert.IsNotNull(zones);
Assert.IsTrue(zones.Length > 0);
}
[TestMethod]
public async Task TestWebAppHomePage()
{
// Arrange
var appHost = await DistributedApplicationTestingBuilder
.CreateAsync<Projects.AppHost>();
appHost.Services.ConfigureHttpClientDefaults(clientBuilder =>
{
clientBuilder.AddStandardResilienceHandler();
});
await using var app = await appHost.BuildAsync();
var resourceNotificationService = app.Services
.GetRequiredService<ResourceNotificationService>();
await app.StartAsync();
// Act
var httpClient = app.CreateHttpClient("myweatherhub");
await resourceNotificationService.WaitForResourceAsync(
"myweatherhub",
KnownResourceStates.Running
)
.WaitAsync(TimeSpan.FromSeconds(30));
var response = await httpClient.GetAsync("/");
// Assert
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
Assert.IsTrue(content.Contains("MyWeatherHub"));
}
}
public record Zone(string Key, string Name, string State);This test class demonstrates how to test your distributed application. Let's examine what these tests are doing:
- Both tests follow a similar pattern, utilizing
DistributedApplicationTestingBuilder.CreateAsync<Projects.AppHost>()to asynchronously create an instance of your application host. - The
appHostis configured with standard HTTP resilience handlers, which provide retry policies and circuit breakers for more robust HTTP communication. - The test calls
appHost.BuildAsync()to build the application and then retrieves theResourceNotificationServicefrom the DI container. - After starting the app with
app.StartAsync(), anHttpClientis created specifically for the resource being tested (either "api" or "myweatherhub"). - The test waits for the target resource to reach the "Running" state before proceeding, ensuring the service is ready to accept requests.
- Finally, HTTP requests are made to specific endpoints, and assertions verify the responses.
In the first test, we verify that the API's /zones endpoint returns a valid collection of zone data. In the second test, we check that the web application's homepage loads successfully and contains the expected content.
The EnvVarTests.cs file verifies environment variable resolution:
namespace MyWeatherHub.Tests;
[TestClass]
public class EnvVarTests
{
[TestMethod]
public async Task WebResourceEnvVarsResolveToApiService()
{
// Arrange
var appHost = await DistributedApplicationTestingBuilder
.CreateAsync<Projects.AppHost>();
var frontend = (IResourceWithEnvironment)appHost.Resources
.Single(static r => r.Name == "myweatherhub");
// Act
var envVars = await frontend.GetEnvironmentVariableValuesAsync(
DistributedApplicationOperation.Publish);
// Assert
CollectionAssert.Contains(envVars,
new KeyValuePair<string, string>(
key: "services__api__https__0",
value: "{api.bindings.https.url}"));
}
}This test focuses on verifying service discovery configuration:
- It uses
DistributedApplicationTestingBuilder.CreateAsync<Projects.AppHost>()to create an instance of the application host. - Instead of starting the application, it directly retrieves an
IResourceWithEnvironmentinstance representing the web frontend ("myweatherhub"). - It calls
GetEnvironmentVariableValuesAsync()with theDistributedApplicationOperation.Publishargument to get the environment variables that would be published to the resource. - Finally, it asserts that the web frontend has an environment variable that resolves to the API service's URL, confirming that service discovery is properly configured.
This test is particularly valuable because it verifies that your application's services are correctly wired together through environment variables, which is how Aspire handles service discovery in distributed applications.
Note: If you see a
WeatherBackgroundTests.csfile in the complete solution that's empty, it's a placeholder for future background job tests and can be ignored for this workshop.
- Open a terminal and navigate to the
completefolder. - Run the integration tests using the
dotnet testcommand:
dotnet test IntegrationTests/IntegrationTests.csproj- Open the solution in Visual Studio
- Open the Test Explorer by going to View > Test Explorer (or press Ctrl+E, T)
- In the Test Explorer window, you'll see all the tests in your solution
- You can:
- Run all tests by clicking the "Run All" button at the top
- Run a specific test by right-clicking it and selecting "Run"
- Run failed tests only by clicking the "Run Failed Tests" button
- Run tests in debug mode by right-clicking and selecting "Debug"
- View test results and output in the Test Explorer window
The tests will verify that:
- Environment variables are properly configured
- The API endpoints are working correctly
- The web application is functioning as expected
When running these tests, all resource logs are redirected to the DistributedApplication by default. This log redirection enables scenarios where you want to assert that a resource is logging correctly, though we're not doing that in these particular tests.
Playwright is a powerful tool for end-to-end testing. It allows you to automate browser interactions and verify that your application works as expected from the user's perspective. Playwright supports multiple browsers, including Chromium, Firefox, and WebKit.
Playwright can be used to perform end-to-end testing of your web application. It can simulate user interactions, such as clicking buttons, filling out forms, and navigating between pages. This ensures that your application behaves correctly in real-world scenarios.
- Browser Automation: Playwright can launch and control browsers to perform automated tests.
- Cross-Browser Testing: Playwright supports testing across different browsers to ensure compatibility.
- Headless Mode: Playwright can run tests in headless mode, which means the browser runs in the background without a graphical user interface.
- Assertions: Playwright provides built-in assertions to verify that elements are present, visible, and have the expected properties.
For more information on Playwright, refer to the official documentation.
In this module, we covered integration testing using Aspire.Hosting.Testing with MSTest. We created a separate test project to test both the API and the web application, following patterns similar to the WebApplicationFactory approach in ASP.NET Core but adapted for distributed applications.
Our tests verified three critical aspects of the distributed application:
- The API functionality (testing that endpoints return expected data)
- The web application functionality (testing that the UI renders correctly)
- The service discovery mechanism (testing that services can find and communicate with each other)
For a deeper dive into testing with Aspire, including a video walkthrough, check out the Getting started with testing and Aspire blog post.
Now, let's learn about deployment options when using Aspire.
Next: Module #9: Deployment
