SuperAPI AI Directory

dotnet-testing-advanced-aspire-testing

Testing & quality

View repository

Updated 10-15 23:45

About this skill

.NET Aspire Testing Integration Testing Framework

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:

  • DbAdapter can now be automatically inferred from the DbConnection type (automatically selects Postgres when using NpgsqlConnection ), but explicit specification is still recommended to ensure correctness
  • Microsoft.Data.SqlClient is 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 FormatDeleteStatement to 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.cs to handle database initialization and Respawn cleanup
  • Generate controller test class (*ControllerTests.cs), inheriting from IntegrationTestBase
  • 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:

Official Documentation

Sample Files

Please refer to the sample files in the same directory:

  • templates/apphost-program.cs - AppHost orchestration definition
  • templates/aspire-app-fixture.cs - Test infrastructure
  • templates/integration-test-collection.cs - Collection Fixture configuration
  • templates/integration-test-base.cs - Test base class
  • templates/database-manager.cs - Database manager
  • templates/controller-tests.cs - Controller test example
  • templates/test-project.csproj - Test project configuration
  • templates/apphost-project.csproj - AppHost project configuration

Related Skills

  • dotnet-testing-advanced-testcontainers-database - Testcontainers database testing
  • dotnet-testing-advanced-testcontainers-nosql - Testcontainers NoSQL testing
  • dotnet-testing-advanced-webapi-integration-testing - Complete WebAPI integration testing