Aspire Testing Orchestrator — .NET Aspire 測試的自動化架構

系列:從鐵人賽到 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 自動管理)
HttpClientfactory.CreateClient()app.CreateHttpClient("servicename")
DbContext 衝突需要 Strategy A/B/C 處理不需要(Aspire 管理 DB 連線)
啟動時間快(單一容器)慢(整個分散式環境,10-15 分鐘)
環境前提DockerDocker + .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.md

Writer 使用的 Aspire Skill

Aspire Writer 透過 mcp-local-rag 語意查詢取得 1 個 Aspire 測試專屬的 Skill:

Skill用途
dotnet-testing-advanced-aspire-testing.NET Aspire 整合測試(DistributedApplicationTestingBuilder)

為什麼只需要一個 Skill?因為 Aspire 測試的模式相對一致 — 不管測什麼服務,都是 DistributedApplicationTestingBuilderCreateHttpClient → 發 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 AnalyzerIntegration AnalyzerAspire Analyzer
分析入口被測類別WebAPI Program.csAppHost Program.cs
分析重點建構子依賴API 端點 + DbContextResource 定義 + Service 依賴
容器需求不涉及NuGet 套件推斷AppHost 宣告式定義
DbContext 分析不涉及註冊模式 A/B/C不需要(Aspire 管理)

分析流程

Aspire Analyzer 的分析分為幾個關鍵步驟:

  1. 定位 AppHost 專案 — 搜尋 .csproj 中包含 <IsAspireHost>true</IsAspireHost>Aspire.AppHost.Sdk
  2. 解析 Resource 定義 — 掃描所有 builder.Add* 呼叫(AddSqlServer、AddDatabase、AddRedis、AddProject 等)
  3. 建立依賴關係圖 — 哪個 Service 參考了哪些 Resource
  4. 分析目標 API 專案 — 分析 Controller 或 Minimal API 的端點
  5. 掃描既有測試基礎設施 — 是否已有 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 aspire

Integration Executor 只需檢查 Docker,Aspire Executor 還需要確認 .NET Aspire workload 已安裝。

超長 Timeout

Aspire 測試的啟動時間遠超其他類型:

類型典型啟動時間原因
單元測試< 5 秒不需要外部資源
Integration10-30 秒啟動 1-2 個容器
Aspire1-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 找不到 ServiceService 名稱與 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 定義的改進中:

  1. launchSettings.json — Aspire 需要 launchSettings.json 來正確啟動,缺少這個檔案會導致端點找不到。這個問題在 Integration Orchestrator 中不會出現。
  2. EnsureCreated — 如果 AppHost 中的資料庫沒有執行 Migration 或 EnsureCreated,測試會因為找不到資料表而失敗。
  3. AwesomeAssertions.WebBe200Ok() 等 Web 專用斷言需要額外安裝 AwesomeAssertions.Web 套件,不包含在基底的 AwesomeAssertions 中。
  4. 修正迴圈上限調整 — 原本設定的 3 輪修正上限對 Aspire 來說不夠,因此將 Aspire Executor 的上限調整為 5 輪。

dotnet-testing-advanced-aspire-orchestrator - 執行狀態

以下是有開啟 dotnet-testing-skills MCP (mcp-local-rag) 的執行結果


三個 Orchestrator 的完整對比

面向Unit Test OrchestratorIntegration OrchestratorAspire Orchestrator
Agent 定義檔5 個5 個5 個
管理的 Skills29 個(透過 mcp-local-rag 查詢)4 個(透過 mcp-local-rag 查詢)1 個(透過 mcp-local-rag 查詢)
Analyzer 入口被測類別WebAPI Program.csAppHost Program.cs
BuilderWebApplicationFactoryDistributedApplicationTestingBuilder
容器管理Testcontainers(程式化)AppHost(宣告式)
Executor 環境檢查DockerDocker + 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,一個使用完全不同測試框架的挑戰。


參考資源

純粹是在寫興趣的,用寫程式、寫文章來抒解工作壓力