Prerequisites
- .NET 8 SDK or later
- Docker Desktop (WSL 2 or Hyper-V)
- AppHost project (.NET Aspire application orchestration)
Core Concepts
.NET Aspire Testing Positioning
.NET Aspire Testing is a closed-box integration testing framework, designed specifically for distributed applications:
- Reproduce the same service architecture in tests as in production
- Use real containers instead of mock services
- Automatically manage container lifecycle
Necessity of the AppHost Project
Using .NET Aspire Testing requires creating an AppHost project:
- Define the complete application architecture and container orchestration
- Tests reuse the AppHost configuration to create the environment
- Without an AppHost, Aspire Testing cannot be used
Differences from Testcontainers
| Feature | .NET Aspire Testing | Testcontainers |
|---|---|---|
| Design Goal | Cloud-native distributed applications | General-purpose container testing |
| Configuration Method | AppHost declarative definition | Manual configuration in code |
| Service Orchestration | Automatic handling | Manual management |
| Learning Curve | Higher | Moderate |
| Applicable Scenarios | Projects already using Aspire | Traditional Web API |
Project Structure
MyApp/
├── src/
│ ├── MyApp.Api/ # WebApi 層
│ ├── MyApp.Application/ # 應用服務層
│ ├── MyApp.Domain/ # 領域模型
│ └── MyApp.Infrastructure/ # 基礎設施層
├── MyApp.AppHost/ # Aspire 編排專案 ⭐
│ ├── MyApp.AppHost.csproj
│ └── Program.cs
└── tests/
└── MyApp.Tests.Integration/ # Aspire Testing 整合測試
├── MyApp.Tests.Integration.csproj
├── Infrastructure/
│ ├── AspireAppFixture.cs
│ ├── IntegrationTestCollection.cs
│ ├── IntegrationTestBase.cs
│ └── DatabaseManager.cs
└── Controllers/
└── MyControllerTests.cs
Required Packages
AppHost Project
<Project Sdk="Microsoft.NET.Sdk">
<Sdk Name="Aspire.AppHost.Sdk" Version="9.0.0" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<IsAspireHost>true</IsAspireHost>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.1.0" />
<PackageReference Include="Aspire.Hosting.PostgreSQL" Version="9.1.0" />
<PackageReference Include="Aspire.Hosting.Redis" Version="9.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\src\MyApp.Api\MyApp.Api.csproj" />
</ItemGroup>
</Project>
Test Project
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting.Testing" Version="13.1.3" />
<PackageReference Include="AwesomeAssertions" Version="9.4.0" />
<PackageReference Include="AwesomeAssertions.Web" Version="1.9.6" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
<PackageReference Include="Respawn" Version="7.0.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\MyApp.AppHost\MyApp.AppHost.csproj" />
</ItemGroup>
</Project>
Container Lifecycle Management
Use ContainerLifetime.Session to ensure test resources are automatically cleaned up:
var postgres = builder.AddPostgres("postgres")
.WithLifetime(ContainerLifetime.Session);
var redis = builder.AddRedis("redis")
.WithLifetime(ContainerLifetime.Session);
- Session: Automatically cleaned up after the test session ends (recommended)
- Persistent: Containers keep running and require manual cleanup
Waiting for Services to Be Ready
Container startup and service readiness are two separate phases and require a waiting mechanism:
private async Task WaitForPostgreSqlReadyAsync()
{
const int maxRetries = 30;
const int delayMs = 1000;
for (int i = 0; i < maxRetries; i++)
{
try
{
var connectionString = await GetConnectionStringAsync();
await using var connection = new NpgsqlConnection(connectionString);
await connection.OpenAsync();
return;
}
catch (Exception ex) when (i < maxRetries - 1)
{
await Task.Delay(delayMs);
}
}
throw new InvalidOperationException("PostgreSQL 服務未能就緒");
}
Database Initialization
Aspire starts the container but does not automatically create the database:
private async Task EnsureDatabaseExistsAsync(string connectionString)
{
var builder = new NpgsqlConnectionStringBuilder(connectionString);
var databaseName = builder.Database;
builder.Database = "postgres"; // 連到預設資料庫
await using var connection = new NpgsqlConnection(builder.ToString());
await connection.OpenAsync();
var checkDbQuery = $"SELECT 1 FROM pg_database WHERE datname = '{databaseName}'";
var dbExists = await new NpgsqlCommand(checkDbQuery, connection).ExecuteScalarAsync();
if (dbExists == null)
{
await new NpgsqlCommand($"CREATE DATABASE \"{databaseName}\"", connection)
.ExecuteNonQueryAsync();
}
}
Respawn Configuration
Specify the adapter when using PostgreSQL:
_respawner = await Respawner.CreateAsync(connection, new RespawnerOptions
{
TablesToIgnore = new Table[] { "__EFMigrationsHistory" },
SchemasToInclude = new[] { "public" },
DbAdapter = DbAdapter.Postgres // 明確指定(7.0 起可從 DbConnection 自動推斷,但建議保留)
});
Respawn 7.0 Upgrade Notes:
DbAdaptercan now be automatically inferred from theDbConnectiontype (automatically selects Postgres when usingNpgsqlConnection), but explicit specification is still recommended to ensure correctnessMicrosoft.Data.SqlClientis no longer a transitive dependency; if you need to reset SQL Server, you must add that package yourself- Added support for SQLite, IBM DB2, and Snowflake adapters
- Added
FormatDeleteStatementto customize the delete statement
Collection Fixture Best Practices
Avoid starting containers repeatedly for each test class:
[CollectionDefinition("Integration Tests")]
public class IntegrationTestCollection : ICollectionFixture<AspireAppFixture>
{
}
[Collection("Integration Tests")]
public class MyControllerTests : IntegrationTestBase
{
public MyControllerTests(AspireAppFixture fixture) : base(fixture) { }
}
Time Testability
Use TimeProvider to abstract time dependencies:
// 服務實作
public class ProductService
{
private readonly TimeProvider _timeProvider;
public ProductService(TimeProvider timeProvider)
{
_timeProvider = timeProvider;
}
public async Task<Product> CreateAsync(ProductCreateRequest request)
{
var now = _timeProvider.GetUtcNow();
var product = new Product
{
CreatedAt = now,
UpdatedAt = now
};
// ...
}
}
// DI 註冊
builder.Services.AddSingleton<TimeProvider>(TimeProvider.System);
Selection Recommendations
Choose .NET Aspire Testing
- The project already uses .NET Aspire
- Need to test interactions between multiple services
- Value a unified development and testing experience
- Cloud-native application architecture
Choose Testcontainers
- Traditional .NET projects
- Requires fine-grained container control
- Integrating with non-.NET services
- The team is unfamiliar with Aspire
Common Issues
Endpoint configuration conflicts
Do not manually configure endpoints that are already handled automatically by Aspire:
// ❌ 錯誤:會造成衝突
builder.AddProject<Projects.MyApp_Api>("my-api")
.WithHttpEndpoint(port: 8080, name: "http");
// ✅ 正確:讓 Aspire 自動處理
builder.AddProject<Projects.MyApp_Api>("my-api")
.WithReference(postgresDb)
.WithReference(redis);
Dapper field mapping
Mapping between PostgreSQL snake_case and C# PascalCase:
// 在 Program.cs 初始化
DapperTypeMapping.Initialize();
// 或使用 SQL 別名
const string sql = @"
SELECT id, name, price,
created_at AS CreatedAt,
updated_at AS UpdatedAt
FROM products";
Output Format
- Generate the AppHost project's
Program.cs, defining container orchestration and service references - Generate test infrastructure files:
AspireAppFixture.cs、IntegrationTestCollection.cs、IntegrationTestBase.cs - Generate
DatabaseManager.csto handle database initialization and Respawn cleanup - Generate controller test class (
*ControllerTests.cs), inheriting fromIntegrationTestBase - Modify the test project
.csproj, adding Aspire.Hosting.Testing and related package references
Reference Resources
Original Articles
The content of this skill is distilled from the "Old-School Software Engineer's Testing Training - 30-Day Challenge" article series:
-
Day 24 - .NET Aspire Testing Introductory Basics
- Ironman series article:https://ithelp.ithome.com.tw/articles/10377071
- Sample code:https://github.com/kevintsengtw/30Days_in_Testing_Samples/tree/main/day24
-
Day 25 - .NET Aspire Integration Testing in Practice: From Testcontainers to .NET Aspire Testing
- Ironman series article:https://ithelp.ithome.com.tw/articles/10377197
- Sample code:https://github.com/kevintsengtw/30Days_in_Testing_Samples/tree/main/day25
Official Documentation
Sample Files
Please refer to the sample files in the same directory:
templates/apphost-program.cs- AppHost orchestration definitiontemplates/aspire-app-fixture.cs- Test infrastructuretemplates/integration-test-collection.cs- Collection Fixture configurationtemplates/integration-test-base.cs- Test base classtemplates/database-manager.cs- Database managertemplates/controller-tests.cs- Controller test exampletemplates/test-project.csproj- Test project configurationtemplates/apphost-project.csproj- AppHost project configuration
Related Skills
dotnet-testing-advanced-testcontainers-database- Testcontainers database testingdotnet-testing-advanced-testcontainers-nosql- Testcontainers NoSQL testingdotnet-testing-advanced-webapi-integration-testing- Complete WebAPI integration testing