--- name: dotnet-testing-advanced-aspire-testing description: | .NET Aspire Testing 整合測試框架完整指南。 涵蓋 AppHost 專案設定、DistributedApplicationTestingBuilder、容器生命週期管理。 包含從 Testcontainers 遷移、多服務編排、Respawn 配置與時間可測試性設計。 triggers: # 核心關鍵字 - aspire testing - .NET Aspire - DistributedApplicationTestingBuilder - AppHost testing - 分散式測試 # 技術術語 - AspireAppFixture - IAsyncLifetime - ContainerLifetime.Session - WithWebHostBuilder - PostgreSqlContainer AspireContainer - RedisContainer Aspire # 使用情境 - 雲原生測試 - 多服務整合 - AppHost 編排 - Aspire.Hosting.Testing - WaitForPostgreSqlReadyAsync - EnsureDatabaseExistsAsync - Respawn DbAdapter.Postgres license: MIT metadata: author: Kevin Tseng version: "1.0.0" tags: "aspire, distributed-testing, cloud-native, testcontainers, integration-testing" --- # .NET Aspire Testing 整合測試框架 ## 適用情境 當被要求執行以下任務時,請使用此技能: - 為 .NET Aspire 分散式應用建立整合測試 - 從 Testcontainers 遷移到 .NET Aspire Testing - 設定 AppHost 專案進行測試 - 使用 DistributedApplicationTestingBuilder 建立測試環境 - 需要測試多個服務間的互動(資料庫、快取、API 等) - 建立雲原生 .NET 應用的整合測試架構 ## 前置需求 - .NET 8 SDK 或更高版本 - Docker Desktop(WSL 2 或 Hyper-V) - AppHost 專案(.NET Aspire 應用編排) ## 核心概念 ### .NET Aspire Testing 定位 **.NET Aspire Testing 是封閉式整合測試框架**,專為分散式應用設計: - 在測試中重現與正式環境相同的服務架構 - 使用真實容器而非模擬服務 - 自動管理容器生命週期 ### AppHost 專案的必要性 使用 .NET Aspire Testing 必須建立 AppHost 專案: - 定義完整的應用架構和容器編排 - 測試重用 AppHost 配置建立環境 - 沒有 AppHost 就無法使用 Aspire Testing ### 與 Testcontainers 的差異 | 特性 | .NET Aspire Testing | Testcontainers | | -------- | ------------------- | -------------- | | 設計目標 | 雲原生分散式應用 | 通用容器測試 | | 配置方式 | AppHost 宣告式定義 | 程式碼手動配置 | | 服務編排 | 自動處理 | 手動管理 | | 學習曲線 | 較高 | 中等 | | 適用場景 | 已用 Aspire 的專案 | 傳統 Web API | ## 專案結構 ```text 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 ``` ## 必要套件 ### AppHost 專案 ```xml Exe net9.0 true ``` ### 測試專案 ```xml net9.0 true ``` ## 容器生命週期管理 使用 `ContainerLifetime.Session` 確保測試資源自動清理: ```csharp var postgres = builder.AddPostgres("postgres") .WithLifetime(ContainerLifetime.Session); var redis = builder.AddRedis("redis") .WithLifetime(ContainerLifetime.Session); ``` - **Session**:測試會話結束後自動清理(推薦) - **Persistent**:容器持續運行,需手動清理 ## 等待服務就緒 容器啟動與服務就緒是兩個階段,需要等待機制: ```csharp 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 服務未能就緒"); } ``` ## 資料庫初始化 Aspire 啟動容器但不自動建立資料庫: ```csharp 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 配置 使用 PostgreSQL 時必須指定適配器: ```csharp _respawner = await Respawner.CreateAsync(connection, new RespawnerOptions { TablesToIgnore = new Table[] { "__EFMigrationsHistory" }, SchemasToInclude = new[] { "public" }, DbAdapter = DbAdapter.Postgres // 關鍵! }); ``` ## Collection Fixture 最佳實踐 避免每個測試類別重複啟動容器: ```csharp [CollectionDefinition("Integration Tests")] public class IntegrationTestCollection : ICollectionFixture { } [Collection("Integration Tests")] public class MyControllerTests : IntegrationTestBase { public MyControllerTests(AspireAppFixture fixture) : base(fixture) { } } ``` ## 時間可測試性 使用 `TimeProvider` 抽象化時間依賴: ```csharp // 服務實作 public class ProductService { private readonly TimeProvider _timeProvider; public ProductService(TimeProvider timeProvider) { _timeProvider = timeProvider; } public async Task CreateAsync(ProductCreateRequest request) { var now = _timeProvider.GetUtcNow(); var product = new Product { CreatedAt = now, UpdatedAt = now }; // ... } } // DI 註冊 builder.Services.AddSingleton(TimeProvider.System); ``` ## 選擇建議 ### 選擇 .NET Aspire Testing - 專案已使用 .NET Aspire - 需要測試多服務互動 - 重視統一的開發測試體驗 - 雲原生應用架構 ### 選擇 Testcontainers - 傳統 .NET 專案 - 需要精細的容器控制 - 與非 .NET 服務整合 - 團隊對 Aspire 不熟悉 ## 常見問題 ### 端點配置衝突 不要手動配置已由 Aspire 自動處理的端點: ```csharp // ❌ 錯誤:會造成衝突 builder.AddProject("my-api") .WithHttpEndpoint(port: 8080, name: "http"); // ✅ 正確:讓 Aspire 自動處理 builder.AddProject("my-api") .WithReference(postgresDb) .WithReference(redis); ``` ### Dapper 欄位映射 PostgreSQL snake_case 與 C# PascalCase 的映射: ```csharp // 在 Program.cs 初始化 DapperTypeMapping.Initialize(); // 或使用 SQL 別名 const string sql = @" SELECT id, name, price, created_at AS CreatedAt, updated_at AS UpdatedAt FROM products"; ``` ## 參考資源 ### 原始文章 本技能內容提煉自「老派軟體工程師的測試修練 - 30 天挑戰」系列文章: - **Day 24 - .NET Aspire Testing 入門基礎介紹** - 鐵人賽文章:https://ithelp.ithome.com.tw/articles/10377071 - 範例程式碼:https://github.com/kevintsengtw/30Days_in_Testing_Samples/tree/main/day24 - **Day 25 - .NET Aspire 整合測試實戰:從 Testcontainers 到 .NET Aspire Testing** - 鐵人賽文章:https://ithelp.ithome.com.tw/articles/10377197 - 範例程式碼:https://github.com/kevintsengtw/30Days_in_Testing_Samples/tree/main/day25 ### 官方文件 - [.NET Aspire 官方文件](https://learn.microsoft.com/dotnet/aspire/) - [Aspire Testing 文件](https://learn.microsoft.com/dotnet/aspire/testing) ## 程式碼範例 請參考同目錄下的範例檔案: - `templates/apphost-program.cs` - AppHost 編排定義 - `templates/aspire-app-fixture.cs` - 測試基礎設施 - `templates/integration-test-collection.cs` - Collection Fixture 設定 - `templates/integration-test-base.cs` - 測試基底類別 - `templates/database-manager.cs` - 資料庫管理員 - `templates/controller-tests.cs` - 控制器測試範例 - `templates/test-project.csproj` - 測試專案設定 - `templates/apphost-project.csproj` - AppHost 專案設定