$npx -y skills add kevintsengtw/dotnet-testing-agent-skills --skill dotnet-testing-advanced-aspire-testing.NET Aspire Testing 整合測試框架完整指南。當需要測試 .NET Aspire 分散式應用程式、設定 AppHost 測試或從 Testcontainers 遷移至 Aspire 測試時使用。涵蓋 DistributedApplicationTestingBuilder、容器生命週期管理、多服務編排、Respawn 配置與時間可測試性設計。 Make sure to use this skill whenever the user mentions .NET Aspire, AppHost testing, DistributedApp
| 1 | # .NET Aspire Testing 整合測試框架 |
| 2 | |
| 3 | ## 前置需求 |
| 4 | |
| 5 | - .NET 8 SDK 或更高版本 |
| 6 | - Docker Desktop(WSL 2 或 Hyper-V) |
| 7 | - AppHost 專案(.NET Aspire 應用編排) |
| 8 | |
| 9 | ## 核心概念 |
| 10 | |
| 11 | ### .NET Aspire Testing 定位 |
| 12 | |
| 13 | **.NET Aspire Testing 是封閉式整合測試框架**,專為分散式應用設計: |
| 14 | |
| 15 | - 在測試中重現與正式環境相同的服務架構 |
| 16 | - 使用真實容器而非模擬服務 |
| 17 | - 自動管理容器生命週期 |
| 18 | |
| 19 | ### AppHost 專案的必要性 |
| 20 | |
| 21 | 使用 .NET Aspire Testing 必須建立 AppHost 專案: |
| 22 | |
| 23 | - 定義完整的應用架構和容器編排 |
| 24 | - 測試重用 AppHost 配置建立環境 |
| 25 | - 沒有 AppHost 就無法使用 Aspire Testing |
| 26 | |
| 27 | ### 與 Testcontainers 的差異 |
| 28 | |
| 29 | | 特性 | .NET Aspire Testing | Testcontainers | |
| 30 | | -------- | ------------------- | -------------- | |
| 31 | | 設計目標 | 雲原生分散式應用 | 通用容器測試 | |
| 32 | | 配置方式 | AppHost 宣告式定義 | 程式碼手動配置 | |
| 33 | | 服務編排 | 自動處理 | 手動管理 | |
| 34 | | 學習曲線 | 較高 | 中等 | |
| 35 | | 適用場景 | 已用 Aspire 的專案 | 傳統 Web API | |
| 36 | |
| 37 | ## 專案結構 |
| 38 | |
| 39 | ```text |
| 40 | MyApp/ |
| 41 | ├── src/ |
| 42 | │ ├── MyApp.Api/ # WebApi 層 |
| 43 | │ ├── MyApp.Application/ # 應用服務層 |
| 44 | │ ├── MyApp.Domain/ # 領域模型 |
| 45 | │ └── MyApp.Infrastructure/ # 基礎設施層 |
| 46 | ├── MyApp.AppHost/ # Aspire 編排專案 ⭐ |
| 47 | │ ├── MyApp.AppHost.csproj |
| 48 | │ └── Program.cs |
| 49 | └── tests/ |
| 50 | └── MyApp.Tests.Integration/ # Aspire Testing 整合測試 |
| 51 | ├── MyApp.Tests.Integration.csproj |
| 52 | ├── Infrastructure/ |
| 53 | │ ├── AspireAppFixture.cs |
| 54 | │ ├── IntegrationTestCollection.cs |
| 55 | │ ├── IntegrationTestBase.cs |
| 56 | │ └── DatabaseManager.cs |
| 57 | └── Controllers/ |
| 58 | └── MyControllerTests.cs |
| 59 | ``` |
| 60 | |
| 61 | ## 必要套件 |
| 62 | |
| 63 | ### AppHost 專案 |
| 64 | |
| 65 | ```xml |
| 66 | <Project Sdk="Microsoft.NET.Sdk"> |
| 67 | <Sdk Name="Aspire.AppHost.Sdk" Version="9.0.0" /> |
| 68 | |
| 69 | <PropertyGroup> |
| 70 | <OutputType>Exe</OutputType> |
| 71 | <TargetFramework>net9.0</TargetFramework> |
| 72 | <IsAspireHost>true</IsAspireHost> |
| 73 | </PropertyGroup> |
| 74 | |
| 75 | <ItemGroup> |
| 76 | <PackageReference Include="Aspire.Hosting.AppHost" Version="9.1.0" /> |
| 77 | <PackageReference Include="Aspire.Hosting.PostgreSQL" Version="9.1.0" /> |
| 78 | <PackageReference Include="Aspire.Hosting.Redis" Version="9.1.0" /> |
| 79 | </ItemGroup> |
| 80 | |
| 81 | <ItemGroup> |
| 82 | <ProjectReference Include="..\src\MyApp.Api\MyApp.Api.csproj" /> |
| 83 | </ItemGroup> |
| 84 | </Project> |
| 85 | ``` |
| 86 | |
| 87 | ### 測試專案 |
| 88 | |
| 89 | ```xml |
| 90 | <Project Sdk="Microsoft.NET.Sdk"> |
| 91 | <PropertyGroup> |
| 92 | <TargetFramework>net9.0</TargetFramework> |
| 93 | <IsTestProject>true</IsTestProject> |
| 94 | </PropertyGroup> |
| 95 | |
| 96 | <ItemGroup> |
| 97 | <PackageReference Include="Aspire.Hosting.Testing" Version="13.1.3" /> |
| 98 | <PackageReference Include="AwesomeAssertions" Version="9.4.0" /> |
| 99 | <PackageReference Include="AwesomeAssertions.Web" Version="1.9.6" /> |
| 100 | <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.0" /> |
| 101 | <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" /> |
| 102 | <PackageReference Include="Respawn" Version="7.0.0" /> |
| 103 | <PackageReference Include="xunit" Version="2.9.3" /> |
| 104 | <PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" /> |
| 105 | </ItemGroup> |
| 106 | |
| 107 | <ItemGroup> |
| 108 | <ProjectReference Include="..\..\MyApp.AppHost\MyApp.AppHost.csproj" /> |
| 109 | </ItemGroup> |
| 110 | </Project> |
| 111 | ``` |
| 112 | |
| 113 | ## 容器生命週期管理 |
| 114 | |
| 115 | 使用 `ContainerLifetime.Session` 確保測試資源自動清理: |
| 116 | |
| 117 | ```csharp |
| 118 | var postgres = builder.AddPostgres("postgres") |
| 119 | .WithLifetime(ContainerLifetime.Session); |
| 120 | |
| 121 | var redis = builder.AddRedis("redis") |
| 122 | .WithLifetime(ContainerLifetime.Session); |
| 123 | ``` |
| 124 | |
| 125 | - **Session**:測試會話結束後自動清理(推薦) |
| 126 | - **Persistent**:容器持續運行,需手動清理 |
| 127 | |
| 128 | ## 等待服務就緒 |
| 129 | |
| 130 | 容器啟動與服務就緒是兩個階段,需要等待機制: |
| 131 | |
| 132 | ```csharp |
| 133 | private async Task WaitForPostgreSqlReadyAsync() |
| 134 | { |
| 135 | const int maxRetries = 30; |
| 136 | const int delayMs = 1000; |
| 137 | |
| 138 | for (int i = 0; i < maxRetries; i++) |
| 139 | { |
| 140 | try |
| 141 | { |
| 142 | var connectionString = await GetConnectionStringAsync(); |
| 143 | await using var connection = new NpgsqlConnection(connectionString); |
| 144 | await connection.OpenAsync(); |
| 145 | return; |
| 146 | } |
| 147 | catch (Exception ex) when (i < maxRetries - 1) |
| 148 | { |
| 149 | await Task.Delay(delayMs); |
| 150 | } |
| 151 | } |
| 152 | throw new InvalidOperationException("PostgreSQL 服務未能就緒"); |
| 153 | } |
| 154 | ``` |
| 155 | |
| 156 | ## 資料庫初始化 |
| 157 | |
| 158 | Aspire 啟動容器但不自動建立資料庫: |
| 159 | |
| 160 | ```csharp |
| 161 | private async Task EnsureDatabaseExistsAsync(string connectionString) |
| 162 | { |
| 163 | var builder = new NpgsqlConnectionStringBuilder(connectionString); |
| 164 | var databaseName = builder.Database; |
| 165 | builder.Database = "postgres"; // 連到預設資料庫 |
| 166 | |
| 167 | await using var connection = new NpgsqlConnection(builder.ToString()); |
| 168 | await connec |