最初のテストを書く
この記事では、Aspire ソリューション向けにテストプロジェクトを作成し、C# でテストを書き、それらを実行する方法を学びます。これらはユニットテストではなく、分散アプリケーションの各コンポーネントがどのように連携して動作するかを検証するための 機能テストおよび統合テスト です。Aspire では、xUnit.net、MSTest、NUnit 向けのテストプロジェクトテンプレートが提供されており、それぞれにサンプルテストが含まれているため、すぐに始めることができます。
分散アプリケーションテストの考え方
Section titled “分散アプリケーションテストの考え方”Aspire ソリューションのテストを始める前に、📦 Aspire.Hosting.Testing NuGet パッケージが必要です。この強力なパッケージは、分散アプリケーション用のテストホストを作成するための入り口となる DistributedApplicationTestingBuilder クラスを提供します。
DistributedApplicationTestingBuilder は、組み込みの計測機能を備えた状態で AppHost プロジェクトを起動するテストハーネスと考えることができます。これにより、ライフサイクル全体を通してホストにアクセスし、操作するための細かな制御が可能になります。IDistributedApplicationBuilder や DistributedApplication といった馴染みのある Aspire の型を使って AppHost を構築・起動できるため、自然で直感的な形でテストを記述できます。
テストプロジェクトの作成
Section titled “テストプロジェクトの作成”Aspire のテストプロジェクトを作成するには、テスト用のプロジェクトテンプレートを使用します。新しい Aspire プロジェクトを開始する際、一部のテンプレートでは IDE や CLI のツールからテストプロジェクトを作成するかどうかを尋ねられます。既存の Aspire ソリューションにテストプロジェクトを追加する場合は、dotnet new コマンドを使用します:
dotnet new aspire-xunit -o xUnit.Testsdotnet new aspire-mstest -o MSTest.Testsdotnet new aspire-nunit -o NUnit.Tests新しく作成されたテストプロジェクトのディレクトリに移動します:
cd xUnit.Testscd MSTest.Testscd NUnit.Testsテストプロジェクトを Aspire ソリューションに追加した後、対象となる AppHost へのプロジェクト参照を追加します。たとえば、Aspire ソリューションに AspireApp.AppHost という名前の AppHost プロジェクトが含まれている場合、次のようにテストプロジェクトから参照を追加します:
dotnet reference add ../AspireApp.AppHost/AspireApp.AppHost.csproj --project xUnit.Tests.csprojdotnet reference add ../AspireApp.AppHost/AspireApp.AppHost.csproj --project MSTest.Tests.csprojdotnet reference add ../AspireApp.AppHost/AspireApp.AppHost.csproj --project NUnit.Tests.csproj最後に、テストプロジェクト内の IntegrationTest1.cs ファイルのコメントアウトを解除すると、サンプルテストを確認できます。
テストプロジェクトを確認する
Section titled “テストプロジェクトを確認する”次のサンプル テストプロジェクトは、Blazor & Minimal API starter テンプレートの一部として作成されたものです。もしこのテンプレートに馴染みがない場合は、最初のアプリを作成する — C#をご参照ください。Aspire のテストプロジェクトは、対象となる AppHost に対してプロジェクト参照の依存関係を持ちます。以下はテンプレート プロジェクトの例です:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <TargetFramework>net10.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <IsPackable>false</IsPackable> <IsTestProject>true</IsTestProject> </PropertyGroup>
<ItemGroup> <PackageReference Include="Aspire.Hosting.Testing" Version="13.0.0" /> <PackageReference Include="coverlet.collector" Version="6.0.4" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" /> <PackageReference Include="xunit" Version="2.9.3" /> <PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" /> </ItemGroup>
<ItemGroup> <ProjectReference Include="..\AspireApp.AppHost\AspireApp.AppHost.csproj" /> </ItemGroup>
<ItemGroup> <Using Include="System.Net" /> <Using Include="Microsoft.Extensions.DependencyInjection" /> <Using Include="Aspire.Hosting.ApplicationModel" /> <Using Include="Aspire.Hosting.Testing" /> <Using Include="Xunit" /> </ItemGroup>
</Project><Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <TargetFramework>net10.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <IsPackable>false</IsPackable> <IsTestProject>true</IsTestProject> <EnableMSTestRunner>true</EnableMSTestRunner> <OutputType>Exe</OutputType> </PropertyGroup>
<ItemGroup> <PackageReference Include="Aspire.Hosting.Testing" Version="13.0.0" /> <PackageReference Include="MSTest" Version="3.10.2" /> </ItemGroup>
<ItemGroup> <ProjectReference Include="..\AspireApp.AppHost\AspireApp.AppHost.csproj" /> </ItemGroup>
<ItemGroup> <Using Include="System.Net" /> <Using Include="Microsoft.Extensions.DependencyInjection" /> <Using Include="Aspire.Hosting.ApplicationModel" /> <Using Include="Aspire.Hosting.Testing" /> <Using Include="Microsoft.VisualStudio.TestTools.UnitTesting" /> </ItemGroup>
</Project><Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <TargetFramework>net10.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <IsPackable>false</IsPackable> <IsTestProject>true</IsTestProject> </PropertyGroup>
<ItemGroup> <PackageReference Include="Aspire.Hosting.Testing" Version="13.0.0" /> <PackageReference Include="coverlet.collector" Version="6.0.4" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" /> <PackageReference Include="NUnit" Version="4.4.0" /> <PackageReference Include="NUnit.Analyzers" Version="4.10.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.6.0" /> </ItemGroup>
<ItemGroup> <ProjectReference Include="..\AspireApp.AppHost\AspireApp.AppHost.csproj" /> </ItemGroup>
<ItemGroup> <Using Include="System.Net" /> <Using Include="Microsoft.Extensions.DependencyInjection" /> <Using Include="Aspire.Hosting.ApplicationModel" /> <Using Include="Aspire.Hosting.Testing" /> <Using Include="NUnit.Framework" /> </ItemGroup>
</Project>上記のプロジェクト ファイルは、比較的一般的な構成になっています。📦 Aspire.Hosting.Testing NuGet パッケージへの PackageReference が含まれており、これには Aspire プロジェクト向けのテストを記述するために必要な型がすべて含まれています。
テンプレートのテストプロジェクトには、1 つのテストを含む IntegrationTest1 クラスが用意されています。このテストでは、次のシナリオを検証します:
- AppHost が正常に作成され、起動できること
webfrontendリソースが利用可能で、実行中であることwebfrontendリソースに対して HTTP リクエストを送信でき、成功レスポンス(HTTP 200 OK)が返ること
Consider the following test class:
using Microsoft.Extensions.Logging;
namespace xUnit.Tests.Tests;
public class IntegrationTest1{ private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(30);
[Fact] public async Task GetWebResourceRootReturnsOkStatusCode() { // 準備(Arrange) var cancellationToken = CancellationToken.None; var appHost = await DistributedApplicationTestingBuilder .CreateAsync<Projects.AspireApp_AppHost>(cancellationToken); appHost.Services.AddLogging(logging => { logging.SetMinimumLevel(LogLevel.Debug); // アプリの構成からのログ フィルターを上書き logging.AddFilter(appHost.Environment.ApplicationName, LogLevel.Debug); logging.AddFilter("Aspire.", LogLevel.Debug); }); appHost.Services.ConfigureHttpClientDefaults(clientBuilder => { clientBuilder.AddStandardResilienceHandler(); });
await using var app = await appHost.BuildAsync(cancellationToken) .WaitAsync(DefaultTimeout, cancellationToken); await app.StartAsync(cancellationToken) .WaitAsync(DefaultTimeout, cancellationToken);
// 実行(Act) using var httpClient = app.CreateHttpClient("webfrontend"); await app.ResourceNotifications.WaitForResourceHealthyAsync( "webfrontend", cancellationToken) .WaitAsync(DefaultTimeout, cancellationToken); using var response = await httpClient.GetAsync("/", cancellationToken);
// 検証(Assert) Assert.Equal(HttpStatusCode.OK, response.StatusCode); }}using Microsoft.Extensions.Logging;
namespace MSTest.Tests;
[TestClass]public class IntegrationTest1{ public TestContext TestContext { get; set; }
private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(30);
[TestMethod] public async Task GetWebResourceRootReturnsOkStatusCode() { // 準備(Arrange) var cancellationToken = TestContext.CancellationTokenSource.Token; var appHost = await DistributedApplicationTestingBuilder .CreateAsync<Projects.AspireApp_AppHost>(); appHost.Services.AddLogging(logging => { logging.SetMinimumLevel(LogLevel.Debug); // アプリの構成からのログ フィルターを上書き logging.AddFilter(appHost.Environment.ApplicationName, LogLevel.Debug); logging.AddFilter("Aspire.", LogLevel.Debug); }); appHost.Services.ConfigureHttpClientDefaults(clientBuilder => { clientBuilder.AddStandardResilienceHandler(); });
await using var app = await appHost.BuildAsync(cancellationToken) .WaitAsync(DefaultTimeout, cancellationToken); await app.StartAsync(cancellationToken) .WaitAsync(DefaultTimeout, cancellationToken);
// 実行(Act) using var httpClient = app.CreateHttpClient("webfrontend"); await app.ResourceNotifications.WaitForResourceHealthyAsync( "webfrontend", cancellationToken) .WaitAsync(DefaultTimeout, cancellationToken); using var response = await httpClient.GetAsync("/", cancellationToken);
// 検証(Assert) Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); }}using Microsoft.Extensions.Logging;
namespace NUnit.Tests;
public class IntegrationTest1{ private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(30);
[Test] public async Task GetWebResourceRootReturnsOkStatusCode() { // 準備(Arrange) using var cts = new CancellationTokenSource(DefaultTimeout); var cancellationToken = cts.Token; var appHost = await DistributedApplicationTestingBuilder .CreateAsync<Projects.AspireApp_AppHost>(); appHost.Services.AddLogging(logging => { logging.SetMinimumLevel(LogLevel.Debug); // アプリの構成からのログ フィルターを上書き logging.AddFilter(appHost.Environment.ApplicationName, LogLevel.Debug); logging.AddFilter("Aspire.", LogLevel.Debug); }); appHost.Services.ConfigureHttpClientDefaults(clientBuilder => { clientBuilder.AddStandardResilienceHandler(); });
await using var app = await appHost.BuildAsync(cancellationToken) .WaitAsync(DefaultTimeout, cancellationToken); await app.StartAsync(cancellationToken) .WaitAsync(DefaultTimeout, cancellationToken);
// 実行(Act) using var httpClient = app.CreateHttpClient("webfrontend"); await app.ResourceNotifications.WaitForResourceHealthyAsync( "webfrontend", cancellationToken) .WaitAsync(DefaultTimeout, cancellationToken); using var response = await httpClient.GetAsync("/", cancellationToken);
// 検証(Assert) Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK)); }}上記のコードでは、次のことを行っています:
DistributedApplicationTestingBuilder.CreateAsyncAPI を使用して、AppHost を非同期に作成しています。appHostはIDistributedApplicationTestingBuilderのインスタンスで、AppHost を表します。- appHost のサービス コレクションに対して、ログ設定および標準の HTTP レジリエンス ハンドラーを構成しています。詳細については 回復性がある HTTP アプリを構築する: 主要な開発パターンをご参照ください。
- The
appHosthas itsBuildAsyncmethod invoked, which returns theDistributedApplicationinstance as theapp. appHost.BuildAsyncを呼び出し、DistributedApplicationインスタンスをappとして取得しています。appを非同期に起動しています。app.CreateHttpClientを呼び出して、webfrontendリソース用のHttpClientを作成しています。app.ResourceNotificationsを使用して、webfrontendリソースが正常状態になるまで待機しています。- A simple HTTP GET request is made to the root of the
webfrontendresource. webfrontendリソースのルートに対して、シンプルな HTTP GET リクエストを送信しています。- The test asserts that the response status code is
OK. - レスポンスのステータス コードが OK(HTTP 200)であることを検証しています。
リソースの環境変数をテストする
Section titled “リソースの環境変数をテストする”Aspire ソリューション内のリソースと、そのリソースが表現している依存関係をさらにテストするために、環境変数が正しく注入されているかを検証できます。次の例では、webfrontend リソースに apiservice リソースへ解決される HTTPS の環境変数が存在することをテストする方法を示します:
using Aspire.Hosting;
namespace Tests;
public class EnvVarTests{ [Fact] public async Task WebResourceEnvVarsResolveToApiService() { // 準備(Arrange) var builder = await DistributedApplicationTestingBuilder .CreateAsync<Projects.AspireApp_AppHost>();
var frontend = builder.CreateResourceBuilder<ProjectResource>("webfrontend");
// 実行(Act) var envVars = await frontend.Resource.GetEnvironmentVariableValuesAsync( DistributedApplicationOperation.Publish);
// 検証(Assert) Assert.Contains(envVars, static (kvp) => { var (key, value) = kvp;
return key is "APISERVICE_HTTPS" && value is "{apiservice.bindings.https.url}"; }); }}using Aspire.Hosting;
namespace Tests;
[TestClass]public class EnvVarTests{ [TestMethod] public async Task WebResourceEnvVarsResolveToApiService() { // 準備(Arrange) var builder = await DistributedApplicationTestingBuilder .CreateAsync<Projects.AspireApp_AppHost>();
var frontend = builder.CreateResourceBuilder<ProjectResource>("webfrontend");
// 実行(Act) var envVars = await frontend.Resource.GetEnvironmentVariableValuesAsync( DistributedApplicationOperation.Publish);
// 検証(Assert) CollectionAssert.Contains(envVars, new KeyValuePair<string, string>( key: "APISERVICE_HTTPS", value: "{apiservice.bindings.https.url}")); }}using Aspire.Hosting;
namespace Tests;
public class EnvVarTests{ [Test] public async Task WebResourceEnvVarsResolveToApiService() { // 準備(Arrange) var builder = await DistributedApplicationTestingBuilder .CreateAsync<Projects.AspireApp_AppHost>();
var frontend = builder.CreateResourceBuilder<ProjectResource>("webfrontend");
// 実行(Act) var envVars = await frontend.Resource.GetEnvironmentVariableValuesAsync( DistributedApplicationOperation.Publish);
// 検証(Assert) Assert.That(envVars, Does.Contain( new KeyValuePair<string, string>( key: "APISERVICE_HTTPS", value: "{apiservice.bindings.https.url}"))); }}上記のコードでは、次のことを行っています:
DistributedApplicationTestingBuilder.CreateAsyncAPI を利用して、AppHost を非同期に作成しています。builderインスタンスは、Resourcesプロパティから 「webfrontend」 という名前のIResourceWithEnvironmentインスタンスを取得するために使用されます。webfrontendリソースに対してGetEnvironmentVariableValuesAsyncを呼び出し、構成済みの環境変数を取得しています。GetEnvironmentVariableValuesAsync呼び出し時にDistributedApplicationOperation.Publishを渡し、バインディング式としてリソースに公開(Publish)される環境変数を対象にしています。- 返却された環境変数を用いて、テストでは
webfrontendリソースに、apiserviceリソースへ解決される HTTPS の環境変数が存在することを検証しています。
テストからログを取得する
Section titled “テストからログを取得する”Aspire ソリューションのテストを記述する際、デバッグやテスト実行状況の確認のためにログを取得・表示したい場面があるかと思います。DistributedApplicationTestingBuilder ではサービス コレクションにアクセスできるため、テスト シナリオ向けにログ設定を行うことができます。
ログ プロバイダーを構成する
Section titled “ログ プロバイダーを構成する”テストからログを取得するには、builder.Services に対して AddLogging メソッドを使用し、テスト フレームワーク向けのログ プロバイダーを構成します:
using Microsoft.Extensions.Logging;
namespace Tests;
public class LoggingTest{ [Fact] public async Task GetWebResourceRootReturnsOkStatusCodeWithLogging() { // 準備(Arrange) var builder = await DistributedApplicationTestingBuilder .CreateAsync<Projects.AspireApp_AppHost>();
builder.Services.ConfigureHttpClientDefaults(clientBuilder => { clientBuilder.AddStandardResilienceHandler(); });
// テスト実行時のログを取得するためのログ設定 builder.Services.AddLogging(logging => logging .AddConsole() // ログをコンソールに出力 .AddFilter("Default", LogLevel.Information) .AddFilter("Microsoft.AspNetCore", LogLevel.Warning) .AddFilter("Aspire.Hosting.Dcp", LogLevel.Warning));
await using var app = await builder.BuildAsync();
await app.StartAsync();
// 実行(Act) var httpClient = app.CreateHttpClient("webfrontend");
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); await app.ResourceNotifications.WaitForResourceHealthyAsync( "webfrontend", cts.Token);
var response = await httpClient.GetAsync("/");
// 検証(Assert) Assert.Equal(HttpStatusCode.OK, response.StatusCode); }}using Microsoft.Extensions.Logging;
namespace Tests;
[TestClass]public class LoggingTest{ [TestMethod] public async Task GetWebResourceRootReturnsOkStatusCodeWithLogging() { // 準備(Arrange) var builder = await DistributedApplicationTestingBuilder .CreateAsync<Projects.AspireApp_AppHost>();
builder.Services.ConfigureHttpClientDefaults(clientBuilder => { clientBuilder.AddStandardResilienceHandler(); });
// テスト実行時のログを取得するためのログ設定 builder.Services.AddLogging(logging => logging .AddConsole() // ログをコンソールに出力 .AddFilter("Default", LogLevel.Information) .AddFilter("Microsoft.AspNetCore", LogLevel.Warning) .AddFilter("Aspire.Hosting.Dcp", LogLevel.Warning));
await using var app = await builder.BuildAsync();
await app.StartAsync();
// 実行(Act) var httpClient = app.CreateHttpClient("webfrontend");
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); await app.ResourceNotifications.WaitForResourceHealthyAsync( "webfrontend", cts.Token);
var response = await httpClient.GetAsync("/");
// 検証(Assert) Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); }}using Microsoft.Extensions.Logging;
namespace Tests;
public class LoggingTest{ [Test] public async Task GetWebResourceRootReturnsOkStatusCodeWithLogging() { // 準備(Arrange) var builder = await DistributedApplicationTestingBuilder .CreateAsync<Projects.AspireApp_AppHost>();
builder.Services.ConfigureHttpClientDefaults(clientBuilder => { clientBuilder.AddStandardResilienceHandler(); });
// テスト実行時のログを取得するためのログ設定 builder.Services.AddLogging(logging => logging .AddConsole() // ログをコンソールに出力 .AddFilter("Default", LogLevel.Information) .AddFilter("Microsoft.AspNetCore", LogLevel.Warning) .AddFilter("Aspire.Hosting.Dcp", LogLevel.Warning));
await using var app = await builder.BuildAsync();
await app.StartAsync();
// 実行(Act) var httpClient = app.CreateHttpClient("webfrontend");
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); await app.ResourceNotifications.WaitForResourceHealthyAsync( "webfrontend", cts.Token);
var response = await httpClient.GetAsync("/");
// 検証(Assert) Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK)); }}ログ フィルターを構成する
Section titled “ログ フィルターを構成する”テスト プロジェクトでは、アプリケーション側の appsettings.json の構成が自動的に反映されるわけではありません。そのため、ログ フィルターを明示的に構成する必要があります。これは、インフラストラクチャ関連の大量のログによってテスト出力が埋もれてしまうのを防ぐために、とても重要です。次のスニペットでは、ログ フィルターを明示的に設定しています:
builder.Services.AddLogging(logging => logging .AddFilter("Default", LogLevel.Information) .AddFilter("Microsoft.AspNetCore", LogLevel.Warning) .AddFilter("Aspire.Hosting.Dcp", LogLevel.Warning));この構成では、次のような設定を行っています:
- ほとんどのアプリケーション ログについて、既定のログ レベルを
Informationに設定しています。 - ASP.NET Core のインフラストラクチャ由来のノイズを抑えるため、ログ レベルを
Warningにセットしています。 - Aspire のホスティング基盤に関するログも
Warningレベルに制限し、アプリケーション固有のログに集中できるようにしています。
よく使われるログ関連パッケージ
Section titled “よく使われるログ関連パッケージ”テスト実行中のログ管理を支援するために、テスト フレームワークごとに利用できるログ プロバイダー パッケージが異なります:
xUnit.net では、テストのログ出力は自動的にテスト結果としてキャプチャされません。そのため、ログをテスト出力として扱うには、ITestOutputHelper インターフェイスを使用する必要があります。
xUnit.net では、次のようなログ パッケージの利用が考えられます:
- 📦 MartinCostello.Logging.XUnit -
ILoggerのログをITestOutputHelperの出力として表示できます。 - 📦 Xunit.DependencyInjection.Logging - xUnit.net の依存性注入と統合できます。
- 📦 Serilog.Extensions.Logging.File - ログをファイルに出力します。
- 📦 Microsoft.Extensions.Logging.Console - ログをコンソールに出力します。
MSTest では、次のようなログ パッケージの利用が考えられます:
- 📦 Serilog.Extensions.Logging.File - ログをファイルに出力します。
- 📦 Microsoft.Extensions.Logging.Console - ログをコンソールに出力します。
NUnit では、次のようなログ パッケージの利用が考えられます:
- 📦 Extensions.Logging.NUnit - NUnit フレームワークと統合できます。
- 📦 Serilog.Extensions.Logging.File - ログをファイルに出力します。
- 📦 Microsoft.Extensions.Logging.Console - ログをコンソールに出力します。
Summary
Section titled “Summary”Aspire のテスト プロジェクト テンプレートを使うことで、Aspire ソリューション向けのテスト プロジェクトを簡単に作成できます。テンプレートにはサンプル テストが含まれており、それを出発点として独自のテストを作成できます。DistributedApplicationTestingBuilder は、ASP.NET Core における WebApplicationFactory<T> とよく似たパターンに従って設計されています。分散アプリケーション用のテスト ホストを作成し、それに対してテストを実行できる点が特長です。
また、DistributedApplicationTestingBuilder を使用すると、すべてのリソース ログは既定で DistributedApplication にリダイレクトされます。この仕組みにより、特定のリソースが正しくログを出力しているかどうかをテストで検証する、といったシナリオも実現できます。