Write your first test
Ce contenu n’est pas encore disponible dans votre langue.
In this article, you’ll learn how to create a test project, write C# tests, and run them for your Aspire solutions. These aren’t unit tests—they’re functional and integration tests that validate how your distributed application components work together. Aspire provides testing project templates for xUnit.net, MSTest, and NUnit frameworks, each including a sample test to help you get started quickly.
Conceptualizing distributed app testing
Section titled “Conceptualizing distributed app testing”Before you start testing your Aspire solutions, you’ll need the 📦 Aspire.Hosting.Testing NuGet package. This powerful package provides the DistributedApplicationTestingBuilder class—your gateway to creating a test host for distributed applications.
Think of the DistributedApplicationTestingBuilder as a test harness that launches your AppHost project with built-in instrumentation. This gives you precise control to access and manipulate the host throughout its lifecycle. You’ll work with familiar Aspire types like IDistributedApplicationBuilder and DistributedApplication to build and start your AppHost, making your tests feel natural and intuitive.
Create a test project
Section titled “Create a test project”To create an Aspire test project, use the testing project template. When starting a new Aspire project, both IDE and CLI tooling prompts you to create a test project for some templates. To add a test project to an existing Aspire solution, use the dotnet new command:
dotnet new aspire-xunit -o xUnit.Testsdotnet new aspire-mstest -o MSTest.Testsdotnet new aspire-nunit -o NUnit.TestsChange directory to the newly created test project:
cd xUnit.Testscd MSTest.Testscd NUnit.TestsAfter adding the test project to your Aspire solution, add a project reference to the target AppHost. For example, if your Aspire solution contains an AppHost project named AspireApp.AppHost, add a project reference to it from the test project:
dotnet reference add ../AspireApp.AppHost/AspireApp.AppHost.csproj --project xUnit.Tests.csprojdotnet reference add ../AspireApp.AppHost/AspireApp.AppHost.csproj --project MSTest.Tests.csprojdotnet reference add ../AspireApp.AppHost/AspireApp.AppHost.csproj --project NUnit.Tests.csprojFinally, you can uncomment out the IntegrationTest1.cs file in the test project to explore the sample test.
Explore the test project
Section titled “Explore the test project”The following example test project was created as part of the Blazor & Minimal API starter template. If you’re unfamiliar with it, see Build your first app—C#. The Aspire test project takes a project reference dependency on the target AppHost. Consider the template project:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <TargetFramework>net10.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <IsPackable>false</IsPackable> <IsTestProject>true</IsTestProject> </PropertyGroup>
<ItemGroup> <PackageReference Include="Aspire.Hosting.Testing" Version="13.0.0" /> <PackageReference Include="coverlet.collector" Version="6.0.4" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" /> <PackageReference Include="xunit" Version="2.9.3" /> <PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" /> </ItemGroup>
<ItemGroup> <ProjectReference Include="..\AspireApp.AppHost\AspireApp.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="Xunit" /> </ItemGroup>
</Project><Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <TargetFramework>net10.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <IsPackable>false</IsPackable> <IsTestProject>true</IsTestProject> <EnableMSTestRunner>true</EnableMSTestRunner> <OutputType>Exe</OutputType> </PropertyGroup>
<ItemGroup> <PackageReference Include="Aspire.Hosting.Testing" Version="13.0.0" /> <PackageReference Include="MSTest" Version="3.10.2" /> </ItemGroup>
<ItemGroup> <ProjectReference Include="..\AspireApp.AppHost\AspireApp.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><Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <TargetFramework>net10.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <IsPackable>false</IsPackable> <IsTestProject>true</IsTestProject> </PropertyGroup>
<ItemGroup> <PackageReference Include="Aspire.Hosting.Testing" Version="13.0.0" /> <PackageReference Include="coverlet.collector" Version="6.0.4" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" /> <PackageReference Include="NUnit" Version="4.4.0" /> <PackageReference Include="NUnit.Analyzers" Version="4.10.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.6.0" /> </ItemGroup>
<ItemGroup> <ProjectReference Include="..\AspireApp.AppHost\AspireApp.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="NUnit.Framework" /> </ItemGroup>
</Project>The preceding project file is fairly standard. There’s a PackageReference to the 📦 Aspire.Hosting.Testing NuGet package, which includes the required types to write tests for Aspire projects.
The template test project includes an IntegrationTest1 class with a single test. The test verifies the following scenario:
- The AppHost is successfully created and started.
- The
webfrontendresource is available and running. - An HTTP request can be made to the
webfrontendresource and returns a successful response (HTTP 200 OK).
Consider the following test class:
using Microsoft.Extensions.Logging;
namespace xUnit.Tests.Tests;
public class IntegrationTest1{ private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(30);
[Fact] public async Task GetWebResourceRootReturnsOkStatusCode() { // Arrange var cancellationToken = CancellationToken.None; var appHost = await DistributedApplicationTestingBuilder .CreateAsync<Projects.AspireApp_AppHost>(cancellationToken); appHost.Services.AddLogging(logging => { logging.SetMinimumLevel(LogLevel.Debug); // Override the logging filters from the app's configuration logging.AddFilter(appHost.Environment.ApplicationName, LogLevel.Debug); logging.AddFilter("Aspire.", LogLevel.Debug); }); appHost.Services.ConfigureHttpClientDefaults(clientBuilder => { clientBuilder.AddStandardResilienceHandler(); });
await using var app = await appHost.BuildAsync(cancellationToken) .WaitAsync(DefaultTimeout, cancellationToken); await app.StartAsync(cancellationToken) .WaitAsync(DefaultTimeout, cancellationToken);
// Act using var httpClient = app.CreateHttpClient("webfrontend"); await app.ResourceNotifications.WaitForResourceHealthyAsync( "webfrontend", cancellationToken) .WaitAsync(DefaultTimeout, cancellationToken); using var response = await httpClient.GetAsync("/", cancellationToken);
// Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); }}using Microsoft.Extensions.Logging;
namespace MSTest.Tests;
[TestClass]public class IntegrationTest1{ public TestContext TestContext { get; set; }
private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(30);
[TestMethod] public async Task GetWebResourceRootReturnsOkStatusCode() { // Arrange var cancellationToken = TestContext.CancellationTokenSource.Token; var appHost = await DistributedApplicationTestingBuilder .CreateAsync<Projects.AspireApp_AppHost>(); appHost.Services.AddLogging(logging => { logging.SetMinimumLevel(LogLevel.Debug); // Override the logging filters from the app's configuration logging.AddFilter(appHost.Environment.ApplicationName, LogLevel.Debug); logging.AddFilter("Aspire.", LogLevel.Debug); }); appHost.Services.ConfigureHttpClientDefaults(clientBuilder => { clientBuilder.AddStandardResilienceHandler(); });
await using var app = await appHost.BuildAsync(cancellationToken) .WaitAsync(DefaultTimeout, cancellationToken); await app.StartAsync(cancellationToken) .WaitAsync(DefaultTimeout, cancellationToken);
// Act using var httpClient = app.CreateHttpClient("webfrontend"); await app.ResourceNotifications.WaitForResourceHealthyAsync( "webfrontend", cancellationToken) .WaitAsync(DefaultTimeout, cancellationToken); using var response = await httpClient.GetAsync("/", cancellationToken);
// Assert Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); }}using Microsoft.Extensions.Logging;
namespace NUnit.Tests;
public class IntegrationTest1{ private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(30);
[Test] public async Task GetWebResourceRootReturnsOkStatusCode() { // Arrange using var cts = new CancellationTokenSource(DefaultTimeout); var cancellationToken = cts.Token; var appHost = await DistributedApplicationTestingBuilder .CreateAsync<Projects.AspireApp_AppHost>(); appHost.Services.AddLogging(logging => { logging.SetMinimumLevel(LogLevel.Debug); // Override the logging filters from the app's configuration logging.AddFilter(appHost.Environment.ApplicationName, LogLevel.Debug); logging.AddFilter("Aspire.", LogLevel.Debug); }); appHost.Services.ConfigureHttpClientDefaults(clientBuilder => { clientBuilder.AddStandardResilienceHandler(); });
await using var app = await appHost.BuildAsync(cancellationToken) .WaitAsync(DefaultTimeout, cancellationToken); await app.StartAsync(cancellationToken) .WaitAsync(DefaultTimeout, cancellationToken);
// Act using var httpClient = app.CreateHttpClient("webfrontend"); await app.ResourceNotifications.WaitForResourceHealthyAsync( "webfrontend", cancellationToken) .WaitAsync(DefaultTimeout, cancellationToken); using var response = await httpClient.GetAsync("/", cancellationToken);
// Assert Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK)); }}The preceding code:
- Relies on the
DistributedApplicationTestingBuilder.CreateAsyncAPI to asynchronously create the AppHost. - The
appHostis an instance ofIDistributedApplicationTestingBuilderthat represents the AppHost. - The
appHostinstance has its service collection configured with logging and the standard HTTP resilience handler. For more information, see Build resilient HTTP apps: Key development patterns. - The
appHosthas itsBuildAsyncmethod invoked, which returns theDistributedApplicationinstance as theapp. - The
appis started asynchronously. - An
HttpClientis created for thewebfrontendresource by callingapp.CreateHttpClient. - The
app.ResourceNotificationsis used to wait for thewebfrontendresource to be healthy. - A simple HTTP GET request is made to the root of the
webfrontendresource. - The test asserts that the response status code is
OK.
Test resource environment variables
Section titled “Test resource environment variables”To further test resources and their expressed dependencies in your Aspire solution, you can assert that environment variables are injected correctly. The following example demonstrates how to test that the webfrontend resource has an HTTPS environment variable that resolves to the apiservice resource:
using Aspire.Hosting;
namespace Tests;
public class EnvVarTests{ [Fact] public async Task WebResourceEnvVarsResolveToApiService() { // Arrange var builder = await DistributedApplicationTestingBuilder .CreateAsync<Projects.AspireApp_AppHost>();
var frontend = builder.CreateResourceBuilder<ProjectResource>("webfrontend");
// Act var envVars = await frontend.Resource.GetEnvironmentVariableValuesAsync( DistributedApplicationOperation.Publish);
// Assert Assert.Contains(envVars, static (kvp) => { var (key, value) = kvp;
return key is "APISERVICE_HTTPS" && value is "{apiservice.bindings.https.url}"; }); }}using Aspire.Hosting;
namespace Tests;
[TestClass]public class EnvVarTests{ [TestMethod] public async Task WebResourceEnvVarsResolveToApiService() { // Arrange var builder = await DistributedApplicationTestingBuilder .CreateAsync<Projects.AspireApp_AppHost>();
var frontend = builder.CreateResourceBuilder<ProjectResource>("webfrontend");
// Act var envVars = await frontend.Resource.GetEnvironmentVariableValuesAsync( DistributedApplicationOperation.Publish);
// Assert CollectionAssert.Contains(envVars, new KeyValuePair<string, string>( key: "APISERVICE_HTTPS", value: "{apiservice.bindings.https.url}")); }}using Aspire.Hosting;
namespace Tests;
public class EnvVarTests{ [Test] public async Task WebResourceEnvVarsResolveToApiService() { // Arrange var builder = await DistributedApplicationTestingBuilder .CreateAsync<Projects.AspireApp_AppHost>();
var frontend = builder.CreateResourceBuilder<ProjectResource>("webfrontend");
// Act var envVars = await frontend.Resource.GetEnvironmentVariableValuesAsync( DistributedApplicationOperation.Publish);
// Assert Assert.That(envVars, Does.Contain( new KeyValuePair<string, string>( key: "APISERVICE_HTTPS", value: "{apiservice.bindings.https.url}"))); }}The preceding code:
- Relies on the
DistributedApplicationTestingBuilder.CreateAsyncAPI to asynchronously create the AppHost. - The
builderinstance is used to retrieve anIResourceWithEnvironmentinstance named “webfrontend” from theResourcesproperty. - The
webfrontendresource is used to callGetEnvironmentVariableValuesAsyncto retrieve its configured environment variables. - The
DistributedApplicationOperation.Publishargument is passed when callingGetEnvironmentVariableValuesAsyncto specify environment variables that are published to the resource as binding expressions. - With the returned environment variables, the test asserts that the
webfrontendresource has an HTTPS environment variable that resolves to theapiserviceresource.
Capture logs from tests
Section titled “Capture logs from tests”When writing tests for your Aspire solutions, you might want to capture and view logs to help with debugging and monitoring test execution. The DistributedApplicationTestingBuilder provides access to the service collection, allowing you to configure logging for your test scenarios.
Configure logging providers
Section titled “Configure logging providers”To capture logs from your tests, use the AddLogging method on the builder.Services to configure logging providers specific to your testing framework:
using Microsoft.Extensions.Logging;
namespace Tests;
public class LoggingTest{ [Fact] public async Task GetWebResourceRootReturnsOkStatusCodeWithLogging() { // Arrange var builder = await DistributedApplicationTestingBuilder .CreateAsync<Projects.AspireApp_AppHost>();
builder.Services.ConfigureHttpClientDefaults(clientBuilder => { clientBuilder.AddStandardResilienceHandler(); });
// Configure logging to capture test execution logs builder.Services.AddLogging(logging => logging .AddConsole() // Outputs logs to console .AddFilter("Default", LogLevel.Information) .AddFilter("Microsoft.AspNetCore", LogLevel.Warning) .AddFilter("Aspire.Hosting.Dcp", LogLevel.Warning));
await using var app = await builder.BuildAsync();
await app.StartAsync();
// Act var httpClient = app.CreateHttpClient("webfrontend");
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); await app.ResourceNotifications.WaitForResourceHealthyAsync( "webfrontend", cts.Token);
var response = await httpClient.GetAsync("/");
// Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); }}using Microsoft.Extensions.Logging;
namespace Tests;
[TestClass]public class LoggingTest{ [TestMethod] public async Task GetWebResourceRootReturnsOkStatusCodeWithLogging() { // Arrange var builder = await DistributedApplicationTestingBuilder .CreateAsync<Projects.AspireApp_AppHost>();
builder.Services.ConfigureHttpClientDefaults(clientBuilder => { clientBuilder.AddStandardResilienceHandler(); });
// Configure logging to capture test execution logs builder.Services.AddLogging(logging => logging .AddConsole() // Outputs logs to console .AddFilter("Default", LogLevel.Information) .AddFilter("Microsoft.AspNetCore", LogLevel.Warning) .AddFilter("Aspire.Hosting.Dcp", LogLevel.Warning));
await using var app = await builder.BuildAsync();
await app.StartAsync();
// Act var httpClient = app.CreateHttpClient("webfrontend");
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); await app.ResourceNotifications.WaitForResourceHealthyAsync( "webfrontend", cts.Token);
var response = await httpClient.GetAsync("/");
// Assert Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); }}using Microsoft.Extensions.Logging;
namespace Tests;
public class LoggingTest{ [Test] public async Task GetWebResourceRootReturnsOkStatusCodeWithLogging() { // Arrange var builder = await DistributedApplicationTestingBuilder .CreateAsync<Projects.AspireApp_AppHost>();
builder.Services.ConfigureHttpClientDefaults(clientBuilder => { clientBuilder.AddStandardResilienceHandler(); });
// Configure logging to capture test execution logs builder.Services.AddLogging(logging => logging .AddConsole() // Outputs logs to console .AddFilter("Default", LogLevel.Information) .AddFilter("Microsoft.AspNetCore", LogLevel.Warning) .AddFilter("Aspire.Hosting.Dcp", LogLevel.Warning));
await using var app = await builder.BuildAsync();
await app.StartAsync();
// Act var httpClient = app.CreateHttpClient("webfrontend");
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); await app.ResourceNotifications.WaitForResourceHealthyAsync( "webfrontend", cts.Token);
var response = await httpClient.GetAsync("/");
// Assert Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK)); }}Configure log filters
Section titled “Configure log filters”Since the appsettings.json configuration from your application isn’t automatically replicated in test projects, you need to explicitly configure log filters. This is important to avoid excessive logging from infrastructure components that might overwhelm your test output. The following snippet explicitly configures log filters:
builder.Services.AddLogging(logging => logging .AddFilter("Default", LogLevel.Information) .AddFilter("Microsoft.AspNetCore", LogLevel.Warning) .AddFilter("Aspire.Hosting.Dcp", LogLevel.Warning));The preceding configuration:
- Sets the default log level to
Informationfor most application logs. - Reduces noise from ASP.NET Core infrastructure by setting it to
Warninglevel. - Limits Aspire hosting infrastructure logs to
Warninglevel to focus on application-specific logs.
Popular logging packages
Section titled “Popular logging packages”Different testing frameworks have different logging provider packages available to assist with managing logging during test execution:
xUnit.net doesn’t capture log output from tests as test output. Tests must use the ITestOutputHelper interface to achieve this.
For xUnit.net, consider using one of these logging packages:
- 📦 MartinCostello.Logging.XUnit - Outputs
ILoggerlogs toITestOutputHelperoutput. - 📦 Xunit.DependencyInjection.Logging - Integrates with xUnit.net’s dependency injection.
- 📦 Serilog.Extensions.Logging.File - Writes logs to files.
- 📦 Microsoft.Extensions.Logging.Console - Outputs logs to console.
For MSTest, consider using one of these logging packages:
- 📦 Serilog.Extensions.Logging.File - Writes logs to files.
- 📦 Microsoft.Extensions.Logging.Console - Outputs logs to console.
For NUnit, consider using one of these logging packages:
- 📦 Extensions.Logging.NUnit - Integrates with NUnit framework.
- 📦 Serilog.Extensions.Logging.File - Writes logs to files.
- 📦 Microsoft.Extensions.Logging.Console - Outputs logs to console.
Summary
Section titled “Summary”The Aspire testing project template makes it easier to create test projects for Aspire solutions. The template project includes a sample test that you can use as a starting point for your tests. The DistributedApplicationTestingBuilder follows a familiar pattern to the WebApplicationFactory<T> in ASP.NET Core. It allows you to create a test host for your distributed application and run tests against it.
Finally, when using the DistributedApplicationTestingBuilder all resource logs are redirected to the DistributedApplication by default. The redirection of resource logs enables scenarios where you want to assert that a resource is logging correctly.