Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
In this article, you learn how to create a test project, write tests, and run them for your .NET Aspire solutions. The tests in this article aren't unit tests, but rather functional or integration tests. .NET Aspire includes several variations of testing project templates that you can use to test your .NET Aspire resource dependencies—and their communications. The testing project templates are available for MSTest, NUnit, and xUnit testing frameworks and include a sample test that you can use as a starting point for your tests.
The .NET Aspire test project templates rely on the 📦 Aspire.Hosting.Testing NuGet package. This package exposes the DistributedApplicationTestingBuilder class, which is used to create a test host for your distributed application. The distributed application testing builder launches your AppHost project with instrumentation hooks so that you can access and manipulate the host at various stages of its lifecyle. In particular, DistributedApplicationTestingBuilder provides you access to IDistributedApplicationBuilder and DistributedApplication class to create and start the AppHost.
Create a test project
The easiest way to create a .NET Aspire test project is to use the testing project template. If you're starting a new .NET Aspire project and want to include test projects, the Visual Studio tooling supports that option. If you're adding a test project to an existing .NET Aspire project, you can use the dotnet new
command to create a test project:
dotnet new aspire-xunit
dotnet new aspire-mstest
dotnet new aspire-nunit
For more information, see the .NET CLI dotnet new command documentation.
Explore the test project
The following example test project was created as part of the .NET Aspire Starter Application template. If you're unfamiliar with it, see Quickstart: Build your first .NET Aspire project. The .NET Aspire test project takes a project reference dependency on the target AppHost. Consider the template project:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting.Testing" Version="9.4.1" />
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.8" />
<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>net9.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="9.4.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.8" />
<PackageReference Include="MSTest" Version="3.10.3" />
</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>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting.Testing" Version="9.4.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.8" />
<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="5.1.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 .NET Aspire projects.
The template test project includes a IntegrationTest1
class with a single test. The test verifies the following scenario:
- The AppHost is successfully created and started.
- The
webfrontend
resource is available and running. - An HTTP request can be made to the
webfrontend
resource and returns a successful response (HTTP 200 OK).
Consider the following test class:
namespace AspireApp.Tests;
public class IntegrationTest1
{
[Fact]
public async Task GetWebResourceRootReturnsOkStatusCode()
{
// Arrange
var builder = await DistributedApplicationTestingBuilder
.CreateAsync<Projects.AspireApp_AppHost>();
builder.Services.ConfigureHttpClientDefaults(clientBuilder =>
{
clientBuilder.AddStandardResilienceHandler();
});
// To capture logs from your tests, see the "Capture logs from tests" section
// in the documentation or refer to LoggingTest.cs for a complete example
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);
}
}
namespace AspireApp.Tests;
[TestClass]
public class IntegrationTest1
{
[TestMethod]
public async Task GetWebResourceRootReturnsOkStatusCode()
{
// Arrange
var builder = await DistributedApplicationTestingBuilder
.CreateAsync<Projects.AspireApp_AppHost>();
builder.Services.ConfigureHttpClientDefaults(clientBuilder =>
{
clientBuilder.AddStandardResilienceHandler();
});
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);
}
}
namespace AspireApp.Tests;
public class IntegrationTest1
{
[Test]
public async Task GetWebResourceRootReturnsOkStatusCode()
{
// Arrange
var builder = await DistributedApplicationTestingBuilder
.CreateAsync<Projects.AspireApp_AppHost>();
builder.Services.ConfigureHttpClientDefaults(clientBuilder =>
{
clientBuilder.AddStandardResilienceHandler();
});
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));
}
}
The preceding code:
- Relies on the DistributedApplicationTestingBuilder.CreateAsync API to asynchronously create the AppHost.
- The
appHost
is an instance ofIDistributedApplicationTestingBuilder
that represents the AppHost. - The
appHost
instance has its service collection configured with the standard HTTP resilience handler. For more information, see Build resilient HTTP apps: Key development patterns.
- The
- The
appHost
has its IDistributedApplicationTestingBuilder.BuildAsync(CancellationToken) method invoked, which returns theDistributedApplication
instance as theapp
.- The
app
has its service provider get the ResourceNotificationService instance. - The
app
is started asynchronously.
- The
- An HttpClient is created for the
webfrontend
resource by callingapp.CreateHttpClient
. - The
resourceNotificationService
is used to wait for thewebfrontend
resource to be available and running. - A simple HTTP GET request is made to the root of the
webfrontend
resource. - The test asserts that the response status code is
OK
.
Test resource environment variables
To further test resources and their expressed dependencies in your .NET 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 AspireApp.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 "services__apiservice__https__0"
&& value is "{apiservice.bindings.https.url}";
});
}
}
using Aspire.Hosting;
namespace AspireApp.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: "services__apiservice__https__0",
value: "{apiservice.bindings.https.url}"));
}
}
using Aspire.Hosting;
namespace AspireApp.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: "services__apiservice__https__0",
value: "{apiservice.bindings.https.url}")));
}
}
The preceding code:
- Relies on the DistributedApplicationTestingBuilder.CreateAsync API to asynchronously create the AppHost.
- The
builder
instance is used to retrieve an IResourceWithEnvironment instance named "webfrontend" from the IDistributedApplicationTestingBuilder.Resources. - The
webfrontend
resource is used to call GetEnvironmentVariableValuesAsync to retrieve its configured environment variables. - The DistributedApplicationOperation.Publish argument is passed when calling
GetEnvironmentVariableValuesAsync
to specify environment variables that are published to the resource as binding expressions. - With the returned environment variables, the test asserts that the
webfrontend
resource has an HTTPS environment variable that resolves to theapiservice
resource.
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
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 AspireApp.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 AspireApp.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 AspireApp.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
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
Information
for most application logs. - Reduces noise from ASP.NET Core infrastructure by setting it to
Warning
level. - Limits Aspire hosting infrastructure logs to
Warning
level to focus on application-specific logs.
Popular logging packages
Different testing frameworks have different logging provider packages available:
For xUnit, consider using one of these logging packages:
- 📦 Xunit.DependencyInjection.Logging - Integrates with xUnit'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:
- 📦 Extensions.Logging.MSTest - Integrates with MSTest framework
- 📦 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
The .NET Aspire testing project template makes it easier to create test projects for .NET 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<TEntryPoint> 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.
See also
.NET Aspire