.NET Aspire + xUnit による統合テスト
このスキルを使用するタイミング
以下の場合にこのスキルを使用します:
- .NET Aspire アプリケーションの統合テストを作成する場合
- 実際のデータベース接続を使用して ASP.NET Core アプリをテストする場合
- 分散アプリケーションにおけるサービス間通信を検証する場合
- コンテナ内の実際のインフラ(SQL Server、Redis、メッセージキュー)を使用したテストを行う場合
- Playwright UI テストと Aspire でオーケストレーションされたサービスを組み合わせる場合
- 適切なサービス検出とネットワークを備えたマイクロサービスをテストする場合
参照ファイル
- advanced-patterns.md:エンドポイント検出、データベーステスト、Playwright、条件付き設定、Respawn、サービス通信、メッセージキュー
- ci-and-tooling.md:CI/CD 統合、カスタムリソースウェイター、MCP を使用した Aspire CLI
基本原則
- 実際の依存関係 - モックではなく、Aspire を介して実際のインフラ(データベース、キャッシュ)を使用する
- 動的ポートバインディング - 競合を避けるために、Aspire にポートを動的に割り当てさせる(
127.0.0.1:0) - フィクスチャのライフサイクル - 適切なテストフィクスチャのセットアップとティアダウンに
IAsyncLifetimeを使用する - エンドポイント検出 - URL をハードコードせず、実行時に Aspire からエンドポイントを取得する
- 並列実行の分離 - xUnit のコレクションを使用してテストの並列化を制御する
- ヘルスチェック - テストを実行する前に、サービスが正常であることを必ず確認する
高レベルテストアーキテクチャ
┌─────────────────┐ ┌──────────────────────┐
│ xUnit テストファイル │──使用───►│ AspireFixture │
└─────────────────┘ │ (IAsyncLifetime) │
└──────────────────────┘
│
│ 起動
▼
┌───────────────────────────┐
│ DistributedApplication │
│ (AppHost から) │
└───────────────────────────┘
│ 公開
▼
┌──────────────────────────────┐
│ 動的 HTTP エンドポイント │
└──────────────────────────────┘
│ 消費
▼
┌─────────────────────────┐
│ HttpClient / Playwright│
└─────────────────────────┘
必須 NuGet パッケージ
<ItemGroup>
<PackageReference Include="Aspire.Hosting.Testing" Version="$(AspireVersion)" />
<PackageReference Include="xunit" Version="*" />
<PackageReference Include="xunit.runner.visualstudio" Version="*" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="*" />
</ItemGroup>
重要: 統合テストのファイルウォッチャー修正
多くの統合テストを実行し、それぞれが IHost を起動すると、デフォルトの .NET ホストビルダーは設定再読み込みのためにファイルウォッチャーを有効にします。これにより、Linux でファイル記述子の制限が枯渇します。
テストを実行する前に、テストプロジェクトに以下を追加してください:
// TestEnvironmentInitializer.cs
using System.Runtime.CompilerServices;
namespace YourApp.Tests;
internal static class TestEnvironmentInitializer
{
[ModuleInitializer]
internal static void Initialize()
{
// テストホストでの設定ファイル監視を無効化
// Linux でのファイル記述子枯渇(inotify 監視上限)を防止
Environment.SetEnvironmentVariable("DOTNET_HOSTBUILDER__RELOADCONFIGONCHANGE", "false");
}
}
パターン1: 基本的な Aspire テストフィクスチャ(モダン API)
using Aspire.Hosting;
using Aspire.Hosting.Testing;
public sealed class AspireAppFixture : IAsyncLifetime
{
private DistributedApplication? _app;
public DistributedApplication App => _app
?? throw new InvalidOperationException("アプリが初期化されていません");
public async Task InitializeAsync()
{
var builder = await DistributedApplicationTestingBuilder
.CreateAsync<Projects.YourApp_AppHost>([
"YourApp:UseVolumes=false",
"YourApp:Environment=IntegrationTest",
"YourApp:Replicas=1"
]);
_app = await builder.BuildAsync();
using var startupCts = new CancellationTokenSource(TimeSpan.FromMinutes(10));
await _app.StartAsync(startupCts.Token);
using var healthCts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
await _app.ResourceNotifications.WaitForResourceHealthyAsync("api", healthCts.Token);
}
public Uri GetEndpoint(string resourceName, string scheme = "https")
{
return _app?.GetEndpoint(resourceName, scheme)
?? throw new InvalidOperationException($"'{resourceName}' のエンドポイントが見つかりません");
}
public async Task DisposeAsync()
{
if (_app is not null)
{
await _app.DisposeAsync();
}
}
}
パターン2: テストでのフィクスチャの使用
[CollectionDefinition("Aspire collection")]
public class AspireCollection : ICollectionFixture<AspireAppFixture> { }
[Collection("Aspire collection")]
public class IntegrationTests
{
private readonly AspireAppFixture _fixture;
public IntegrationTests(AspireAppFixture fixture)
{
_fixture = fixture;
}
[Fact]
public async Task Application_ShouldStart()
{
var httpClient = _fixture.App.CreateHttpClient("yourapp");
var response = await httpClient.GetAsync("/");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
}
詳細なパターン(エンドポイント検出、データベーステスト、Playwright UI テスト、条件付きリソース設定、Respawn によるデータベースリセット、サービス間通信、メッセージキューテスト)については、advanced-patterns.md を参照してください。
一般的なパターンまとめ
| パターン | ユースケース |
|---|---|
| 基本的なフィクスチャ | シンプルな HTTP エンドポイントテスト |
| エンドポイント検出 | ハードコードされた URL を回避 |
| データベーステスト | データアクセス層の検証 |
| Playwright 統合 | 実際のバックエンドを使用した完全 UI テスト |
| 設定のオーバーライド | テスト固有の設定 |
| ヘルスチェック | サービスが準備完了であることを確認 |
| サービス通信 | 分散システムの相互作用をテスト |
| メッセージキューテスト | 非同期メッセージングの検証 |
直感的でない / 注意すべきヒント
| 問題 | 解決策 |
|---|---|
| テストがすぐにタイムアウトする | await _app.StartAsync() を呼び出し、サービスが正常になるのを待つ |
| テスト間のポート競合 | xUnit の CollectionDefinition を使用してフィクスチャを共有する |
| タイミングによる不安定なテスト | Task.Delay() ではなく、適切なヘルスチェックポーリングを実装する |
| SQL Server に接続できない | GetConnectionStringAsync() を使用して動的に接続文字列を取得する |
| 並列テストが干渉する | [Collection] 属性を使用して関連テストを逐次実行する |
| Aspire ダッシュボードの競合 | 同時に実行できるダッシュボードは1つのみ。テストは同じインスタンスを再利用する |
ベストプラクティス
IAsyncLifetimeを使用する - 非同期の初期化とクリーンアップを確実に行う- コレクションを介してフィクスチャを共有する - アプリインスタンスを再利用してテスト実行時間を短縮する
- エンドポイントを動的に取得する - localhost:5000 などをハードコードしない
- ヘルスチェックを待つ - サービスがすぐに準備できると想定しない
- 実際の依存関係でテストする - Aspire を使用すると、実際の SQL、Redis などを簡単に使用できる
- リソースをクリーンアップする - 常に
DisposeAsyncを適切に実装する - 意味のあるテストデータを使用する - データベースに現実的なテストデータをシードする
- 障害シナリオをテストする - エラーハンドリングと回復力を検証する
- テストを分離する - 各テストは独立しており、順序に依存しないようにする
- テスト実行時間を監視する - テストが遅い場合は、並列化を検討する
GitHub Actions のセットアップ、カスタムリソースウェイター、Aspire CLI/MCP 統合については ci-and-tooling.md を参照してください。
デバッグのヒント
- Aspire ダッシュボードを実行する - テストが失敗した場合、
http://localhost:15888でダッシュボードを確認する - MCP と共に Aspire CLI を使用する - AI アシスタントに実際のアプリケーション状態を問い合わせさせる
- 詳細なログを有効にする -
ASPIRE_ALLOW_UNSECURED_TRANSPORT=trueを設定してより詳細な出力を得る - コンテナログを確認する -
docker logsを使用してコンテナの出力を検査する - フィクスチャにブレークポイントを設定する - フィクスチャの初期化をデバッグして起動問題を捉える
- リソース名を確認する - AppHost とテスト間でリソース名が一致していることを確認する