系列:從鐵人賽到 Agent Orchestration — AI 自動建立 .NET 測試的完整方案(8)
前言
上一篇介紹了 Integration Test Orchestrator 如何處理 ASP.NET Core WebAPI 的整合測試。但在 .NET 生態系中,還有另一種截然不同的整合測試模式 — .NET Aspire。
Aspire Testing Orchestrator 是原本計畫中就要做的第三個 Orchestrator。架構上參考 Unit Test Orchestrator 的 1 + 4 Subagent 設計,流程與各個 Subagent 的內容則依據 dotnet-testing-agent-skills 中的 dotnet-testing-advanced-aspire-testing Skill 來做設計。
Aspire 的測試不是「啟動 WebApplicationFactory + 掛上 Testcontainers」,而是用 DistributedApplicationTestingBuilder 啟動整個分散式應用,讓 AppHost 自動管理所有的容器和服務。測試的角度從「測一個 API 端點」變成「測整個分散式系統的啟動與運行」——這個根本性的差異,讓每個 Subagent 都針對 Aspire 的特殊性重新設計。
.NET Aspire 測試的特殊性
什麼是 .NET Aspire
.NET Aspire 是微軟推出的雲端原生分散式應用程式開發框架。它的核心概念是 AppHost — 一個集中定義所有服務、資料庫、快取等 Resource 的專案。開發者在 AppHost 的 Program.cs 中宣告式地定義整個分散式應用的拓撲:
var builder = DistributedApplication.CreateBuilder(args);
var sql = builder.AddSqlServer("sql");
var db = sql.AddDatabase("BookingsDb");
var cache = builder.AddRedis("cache");
builder.AddProject<Projects.Practice_Aspire_WebApi>("bookingapi")
.WithReference(db)
.WithReference(cache);
與傳統整合測試的根本差異
| 面向 | Integration Test(上一篇) | Aspire Test(本篇) |
|---|---|---|
| 測試入口 | WebApplicationFactory<Program> | DistributedApplicationTestingBuilder |
| 容器管理 | 程式化(Testcontainers NuGet) | 宣告式(AppHost 自動管理) |
| HttpClient | factory.CreateClient() | app.CreateHttpClient("servicename") |
| DbContext 衝突 | 需要 Strategy A/B/C 處理 | 不需要(Aspire 管理 DB 連線) |
| 啟動時間 | 快(單一容器) | 慢(整個分散式環境,10-15 分鐘) |
| 環境前提 | Docker | Docker + .NET Aspire workload |
| Service 發現 | 固定 localhost | 動態 Resource 名稱 |
最關鍵的差異是:Aspire 測試不需要手動管理容器。Integration Orchestrator 花了大量篇幅處理 Testcontainers 的初始化、Port 衝突、DbContext 註冊衝突 — 這些在 Aspire 測試中完全不存在,因為 AppHost 會自動處理所有容器的啟動和連線設定。
Aspire Orchestrator 架構
5 個專屬 Agent 定義檔
5 個 Agent 定義檔都放在 .github/agents/ 目錄下:
.github/agents/
├── dotnet-testing-advanced-aspire-orchestrator.agent.md
├── dotnet-testing-advanced-aspire-analyzer.agent.md
├── dotnet-testing-advanced-aspire-writer.agent.md
├── dotnet-testing-advanced-aspire-executor.agent.md
└── dotnet-testing-advanced-aspire-reviewer.agent.mdWriter 使用的 Aspire Skill
Aspire Writer 透過 mcp-local-rag 語意查詢取得 1 個 Aspire 測試專屬的 Skill:
| Skill | 用途 |
|---|---|
dotnet-testing-advanced-aspire-testing | .NET Aspire 整合測試(DistributedApplicationTestingBuilder) |
為什麼只需要一個 Skill?因為 Aspire 測試的模式相對一致 — 不管測什麼服務,都是 DistributedApplicationTestingBuilder → CreateHttpClient → 發 HTTP 請求 → 驗證結果。變化的是 Resource 組合,不是測試技術。
JSON 交接機制
v2.0.0 引入了結構化 JSON 交接機制,各 Subagent 的中間結果不再透過 prompt 傳遞,而是寫入本地檔案,下游 Subagent 直接讀檔:
.orchestrator/{TargetName}/
├── analyzer-result.json # Analyzer 的分析報告(含 sourceCodeContext)
├── writer-result.json # Writer 的輸出摘要
└── executor-result.json # Executor 的執行結果Aspire Orchestrator 還運用了 sourceCodeContext 前向傳遞機制——Analyzer 讀取的原始碼(AppHost Program.cs、Controller、Models 等)直接嵌入後續 Writer 和 Reviewer 的委派 prompt,避免重複讀檔。
Phase Timing
v2.0.0 在 Aspire Orchestrator 中內建了階段耗時記錄,每次完整流程結束後會輸出 aspire-orchestrator-timing.log,顯示各階段耗時:
Phase 1 (Analyzer): xx 秒
Phase 2 (Writer): x 分 xx 秒
Phase 3 (Executor): x 分 xx 秒
Phase 4 (Reviewer): xx 秒
─────────────────────
Total: x 分 xx 秒因為 Aspire 測試本身的啟動時間就遠超其他類型(1-5 分鐘),Phase Timing 在這裡的價值特別明顯——可以清楚看出時間花在 AppHost 啟動、測試執行還是其他階段。
四個 Subagent 的差異比較
Analyzer 差異 — AppHost 優先分析
Aspire Analyzer 與其他 Analyzer 最大的不同在於分析入口:它從 AppHost 的 Program.cs 開始,不是從被測類別開始。
| 面向 | Unit Test Analyzer | Integration Analyzer | Aspire Analyzer |
|---|---|---|---|
| 分析入口 | 被測類別 | WebAPI Program.cs | AppHost Program.cs |
| 分析重點 | 建構子依賴 | API 端點 + DbContext | Resource 定義 + Service 依賴 |
| 容器需求 | 不涉及 | NuGet 套件推斷 | AppHost 宣告式定義 |
| DbContext 分析 | 不涉及 | 註冊模式 A/B/C | 不需要(Aspire 管理) |
分析流程
Aspire Analyzer 的分析分為幾個關鍵步驟:
- 定位 AppHost 專案 — 搜尋
.csproj中包含<IsAspireHost>true</IsAspireHost>或Aspire.AppHost.Sdk - 解析 Resource 定義 — 掃描所有
builder.Add*呼叫(AddSqlServer、AddDatabase、AddRedis、AddProject 等) - 建立依賴關係圖 — 哪個 Service 參考了哪些 Resource
- 分析目標 API 專案 — 分析 Controller 或 Minimal API 的端點
- 掃描既有測試基礎設施 — 是否已有 AspireAppFixture、Collection Fixture、DatabaseManager
輸出格式
Aspire Analyzer 的報告中最獨特的欄位是 appHostInfo:
{
"orchestrationType": "aspire",
"appHostInfo": {
"resources": [
{ "name": "sql", "type": "SqlServer" },
{ "name": "BookingsDb", "type": "Database", "parent": "sql" },
{ "name": "cache", "type": "Redis" },
{ "name": "bookingapi", "type": "Project" }
],
"projectReferences": ["Practice.Aspire.WebApi"],
"dependencyGraph": { "bookingapi": ["BookingsDb", "cache"] },
"containerLifetime": "Session"
},
"requiredSkills": ["dotnet-testing-advanced-aspire-testing"]
}注意 requiredSkills 永遠只有一個 aspire-testing — 這是固定的。
Aspire 版本偵測的特殊處理
Aspire 的版本偵測比較特別。Aspire 13.x 使用 <Project Sdk="Aspire.AppHost.Sdk/X.Y.Z"> 格式,而 Aspire 8.x 可能在 NuGet 參考中使用 Aspire.AppHost.Sdk 9.0.0(NuGet 最低版本),但實際 runtime 套件是 8.x。Analyzer 需要正確處理這兩種格式。
Writer 差異 — Fixture 與 Resource 管理
Aspire Writer 的核心工作是建立正確的測試基礎設施,而不是處理容器管理(因為 Aspire 會自動處理)。
測試基礎設施
Writer 需要建立以下結構:
tests/AppHost.Tests/
├── .csproj
├── GlobalUsings.cs
├── Infrastructure/
│ ├── AspireAppFixture.cs # DistributedApplicationTestingBuilder
│ ├── AspireAppCollectionDefinition.cs # Collection Fixture
│ ├── IntegrationTestBase.cs # 測試基底類別
│ └── DatabaseManager.cs # Respawn 資料庫清理
└── Integration/
└── BookingsApiTests.cs # 測試類別AspireAppFixture — 核心差異
這是 Aspire Writer 最關鍵的產出物,與 Integration Writer 的 WebApiFactory 完全不同:
public class AspireAppFixture : IAsyncLifetime
{
public DistributedApplication App { get; private set; } = null!;
public async Task InitializeAsync()
{
var builder = await DistributedApplicationTestingBuilder
.CreateAsync<Projects.Practice_Aspire_AppHost>();
builder.Services.ConfigureHttpClientDefaults(http =>
http.AddStandardResilienceHandler());
App = await builder.BuildAsync();
await App.StartAsync();
await App.WaitForResources().WaitAsync(TimeSpan.FromMinutes(5));
}
public async Task DisposeAsync()
{
await App.StopAsync();
await App.DisposeAsync();
}
}關鍵差異:
- 使用
DistributedApplicationTestingBuilder(不是WebApplicationFactory) WaitForResources()等待所有 Resource 就緒ContainerLifetime設為 Session(容器在整個測試 Session 中共用,不是每個測試重新啟動)
Resource 名稱一致性
Writer 在建立 HttpClient 時,必須使用與 AppHost 中 AddProject 定義的完全相同的 Service 名稱:
// AppHost 定義
builder.AddProject<Projects.Practice_Aspire_WebApi>("bookingapi");
// 測試中建立 HttpClient — 名稱必須完全一致
var client = App.CreateHttpClient("bookingapi");名稱不一致是 Aspire 測試中最常見的錯誤之一。
Executor 差異 — 超長等待與環境檢查
Aspire Executor 有兩個獨特的挑戰:環境前提和執行時間。執行完成後,Executor 會將結果寫入 .orchestrator/{TargetName}/executor-result.json,供 Reviewer 讀取最新測試執行狀態。
Step 0:雙重環境檢查
Step 0a:Docker 環境檢查
│ 執行 docker info
├── Docker 可用 → 繼續
└── Docker 不可用 → 回報錯誤
Step 0b:Aspire workload 檢查
│ 執行 dotnet workload list
├── aspire 已安裝 → 繼續
└── aspire 未安裝 → 回報錯誤,建議 dotnet workload install aspireIntegration Executor 只需檢查 Docker,Aspire Executor 還需要確認 .NET Aspire workload 已安裝。
超長 Timeout
Aspire 測試的啟動時間遠超其他類型:
| 類型 | 典型啟動時間 | 原因 |
|---|---|---|
| 單元測試 | < 5 秒 | 不需要外部資源 |
| Integration | 10-30 秒 | 啟動 1-2 個容器 |
| Aspire | 1-5 分鐘 | 啟動完整分散式環境(多個容器 + Service) |
因此 Executor 的 timeout 設定需要 ≥ 10 分鐘,而且需要 --blame-hang-timeout 參數防止測試被誤判為 hang。
常見錯誤模式
Aspire Executor 需要處理一些 Aspire 特有的錯誤:
| 錯誤 | 原因 | 修正方式 |
|---|---|---|
DistributedApplicationTestingBuilder 找不到 | 缺少 Aspire.Hosting.Testing | 加入 NuGet 套件 |
Projects.xxx 類型不存在 | AppHost 沒有正確參考目標專案 | 檢查 ProjectReference |
| Resource readiness timeout | 容器啟動超時 | 增加 WaitForResources timeout |
CreateHttpClient 找不到 Service | Service 名稱與 AppHost 不一致 | 修正名稱(最常見的錯誤) |
| TLS/SSL 憑證錯誤 | Aspire 13.1.0+ 的新行為 | 設定 StandardResilienceHandler |
| 測試因 Container 重啟而 timeout | 沒有設定 ContainerLifetime | 加入 ContainerLifetime.Session |
Reviewer 差異 — Aspire 特有審查維度
Aspire Reviewer 在標準審查基礎上,新增了 Aspire 特有的檢查項目:
| 審查項目 | 說明 |
|---|---|
| DistributedApplicationTestingBuilder | 是否使用正確的 Builder(不是 WebApplicationFactory) |
| ContainerLifetime.Session | 是否設定容器生命週期為 Session |
| Resource 名稱一致性 | HttpClient 建立時的名稱是否與 AppHost 定義一致 |
| Collection Fixture | 是否使用 Collection Fixture 共享 App instance |
| App 生命週期 | InitializeAsync → Start/Stop → DisposeAsync 是否正確 |
| Respawn 設定 | 資料庫清理機制是否正確 |
| 無 WebApplicationFactory | 確認沒有混用 WebApplicationFactory |
開發過程中的關鍵發現
在開發和調整 Aspire Testing Orchestrator 的過程中,有幾個值得記錄的發現,也反映到了 Agent 定義的改進中:
- launchSettings.json — Aspire 需要
launchSettings.json來正確啟動,缺少這個檔案會導致端點找不到。這個問題在 Integration Orchestrator 中不會出現。 - EnsureCreated — 如果 AppHost 中的資料庫沒有執行 Migration 或 EnsureCreated,測試會因為找不到資料表而失敗。
- AwesomeAssertions.Web —
Be200Ok()等 Web 專用斷言需要額外安裝AwesomeAssertions.Web套件,不包含在基底的AwesomeAssertions中。 - 修正迴圈上限調整 — 原本設定的 3 輪修正上限對 Aspire 來說不夠,因此將 Aspire Executor 的上限調整為 5 輪。
dotnet-testing-advanced-aspire-orchestrator - 執行狀態
以下是有開啟 dotnet-testing-skills MCP (mcp-local-rag) 的執行結果




三個 Orchestrator 的完整對比
| 面向 | Unit Test Orchestrator | Integration Orchestrator | Aspire Orchestrator |
|---|---|---|---|
| Agent 定義檔 | 5 個 | 5 個 | 5 個 |
| 管理的 Skills | 29 個(透過 mcp-local-rag 查詢) | 4 個(透過 mcp-local-rag 查詢) | 1 個(透過 mcp-local-rag 查詢) |
| Analyzer 入口 | 被測類別 | WebAPI Program.cs | AppHost Program.cs |
| Builder | — | WebApplicationFactory | DistributedApplicationTestingBuilder |
| 容器管理 | — | Testcontainers(程式化) | AppHost(宣告式) |
| Executor 環境檢查 | — | Docker | Docker + Aspire workload |
| Executor Timeout | 標準 | 標準 | ≥ 10 分鐘 |
小結
Aspire Testing Orchestrator 的開發體驗與前兩個 Orchestrator 很不一樣。Integration Orchestrator 要處理大量的「衝突」— DbContext 衝突、Type 衝突、Port 衝突。而 Aspire Orchestrator 面對的挑戰是「等待」— 等容器啟動、等 Resource 就緒、等測試完成。
好處是,因為 Aspire 把容器管理的複雜度封裝在 AppHost 裡,Writer 的工作反而變得更單純了 — 只需要一個 Skill、一種測試模式。壞處是,Executor 需要處理超長的等待時間和更多的環境前提。
1 + 4 Subagent 架構再次證明了它的可複製性。不同的測試領域,相同的架構骨架,只需要調整每個 Subagent 的領域知識。下一篇會介紹第四個也是最後一個 Orchestrator — TUnit Testing Orchestrator,一個使用完全不同測試框架的挑戰。
參考資源
- dotnet-testing-agent-orchestration:https://github.com/kevintsengtw/dotnet-testing-agent-orchestration
- Aspire Orchestrator 設計文件:https://github.com/kevintsengtw/dotnet-testing-agent-orchestration/blob/main/docs/agent_orchestration/dotnet-testing-advanced-aspire-orchestrator.md
- Aspire 測試操作指南:https://github.com/kevintsengtw/dotnet-testing-agent-orchestration/blob/main/docs/agent_orchestration/practice-guide-aspire-testing.md
- Aspire 測試驗證專案:https://github.com/kevintsengtw/dotnet-testing-agent-orchestration/tree/main/samples/practice_aspire
- dotnet-testing-agent-skills:https://github.com/kevintsengtw/dotnet-testing-agent-skills
- .NET Aspire 官方文件:https://learn.microsoft.com/dotnet/aspire/
純粹是在寫興趣的,用寫程式、寫文章來抒解工作壓力