コンテンツにスキップ

最初のテストを書く

使用するテストフレームワークを選択してください

この記事では、Aspire ソリューション向けにテストプロジェクトを作成し、C# でテストを書き、それらを実行する方法を学びます。これらはユニットテストではなく、分散アプリケーションの各コンポーネントがどのように連携して動作するかを検証するための 機能テストおよび統合テスト です。Aspire では、xUnit.net、MSTest、NUnit 向けのテストプロジェクトテンプレートが提供されており、それぞれにサンプルテストが含まれているため、すぐに始めることができます。

分散アプリケーションテストの考え方

Section titled “分散アプリケーションテストの考え方”

Aspire ソリューションのテストを始める前に、📦 Aspire.Hosting.Testing NuGet パッケージが必要です。この強力なパッケージは、分散アプリケーション用のテストホストを作成するための入り口となる DistributedApplicationTestingBuilder クラスを提供します。

DistributedApplicationTestingBuilder は、組み込みの計測機能を備えた状態で AppHost プロジェクトを起動するテストハーネスと考えることができます。これにより、ライフサイクル全体を通してホストにアクセスし、操作するための細かな制御が可能になります。IDistributedApplicationBuilderDistributedApplication といった馴染みのある Aspire の型を使って AppHost を構築・起動できるため、自然で直感的な形でテストを記述できます。

Aspire のテストプロジェクトを作成するには、テスト用のプロジェクトテンプレートを使用します。新しい Aspire プロジェクトを開始する際、一部のテンプレートでは IDE や CLI のツールからテストプロジェクトを作成するかどうかを尋ねられます。既存の Aspire ソリューションにテストプロジェクトを追加する場合は、dotnet new コマンドを使用します:

Terminal window
dotnet new aspire-xunit -o xUnit.Tests
Terminal window
dotnet new aspire-mstest -o MSTest.Tests
Terminal window
dotnet new aspire-nunit -o NUnit.Tests

新しく作成されたテストプロジェクトのディレクトリに移動します:

Terminal window
cd xUnit.Tests
Terminal window
cd MSTest.Tests
Terminal window
cd NUnit.Tests

テストプロジェクトを Aspire ソリューションに追加した後、対象となる AppHost へのプロジェクト参照を追加します。たとえば、Aspire ソリューションに AspireApp.AppHost という名前の AppHost プロジェクトが含まれている場合、次のようにテストプロジェクトから参照を追加します:

Terminal window
dotnet reference add ../AspireApp.AppHost/AspireApp.AppHost.csproj --project xUnit.Tests.csproj
Terminal window
dotnet reference add ../AspireApp.AppHost/AspireApp.AppHost.csproj --project MSTest.Tests.csproj
Terminal window
dotnet reference add ../AspireApp.AppHost/AspireApp.AppHost.csproj --project NUnit.Tests.csproj

最後に、テストプロジェクト内の IntegrationTest1.cs ファイルのコメントアウトを解除すると、サンプルテストを確認できます。

テストプロジェクトを確認する

Section titled “テストプロジェクトを確認する”

次のサンプル テストプロジェクトは、Blazor & Minimal API starter テンプレートの一部として作成されたものです。もしこのテンプレートに馴染みがない場合は、最初のアプリを作成する — C#をご参照ください。Aspire のテストプロジェクトは、対象となる AppHost に対してプロジェクト参照の依存関係を持ちます。以下はテンプレート プロジェクトの例です:

xUnit.Tests.csproj
<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>
MSTest.Tests.csproj
<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>
NUnit.Tests.csproj
<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:

IntegrationTest1.cs
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);
}
}
IntegrationTest1.cs
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);
}
}
IntegrationTest1.cs
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.CreateAsync API を使用して、AppHost を非同期に作成しています。
  • appHostIDistributedApplicationTestingBuilder のインスタンスで、AppHost を表します。
  • appHost のサービス コレクションに対して、ログ設定および標準の HTTP レジリエンス ハンドラーを構成しています。詳細については 回復性がある HTTP アプリを構築する: 主要な開発パターンをご参照ください。
  • The appHost has its BuildAsync method invoked, which returns the DistributedApplication instance as the app.
  • appHost.BuildAsync を呼び出し、DistributedApplication インスタンスを app として取得しています。
  • app を非同期に起動しています。
  • app.CreateHttpClient を呼び出して、webfrontend リソース用の HttpClient を作成しています。
  • app.ResourceNotifications を使用して、webfrontend リソースが正常状態になるまで待機しています。
  • A simple HTTP GET request is made to the root of the webfrontend resource.
  • webfrontend リソースのルートに対して、シンプルな HTTP GET リクエストを送信しています。
  • The test asserts that the response status code is OK.
  • レスポンスのステータス コードが OK(HTTP 200)であることを検証しています。

リソースの環境変数をテストする

Section titled “リソースの環境変数をテストする”

Aspire ソリューション内のリソースと、そのリソースが表現している依存関係をさらにテストするために、環境変数が正しく注入されているかを検証できます。次の例では、webfrontend リソースに apiservice リソースへ解決される HTTPS の環境変数が存在することをテストする方法を示します:

EnvVarTests.cs
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}";
});
}
}
EnvVarTests.cs
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}"));
}
}
EnvVarTests.cs
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.CreateAsync API を利用して、AppHost を非同期に作成しています。
  • builder インスタンスは、Resources プロパティから 「webfrontend」 という名前の IResourceWithEnvironment インスタンスを取得するために使用されます。
  • webfrontend リソースに対して GetEnvironmentVariableValuesAsync を呼び出し、構成済みの環境変数を取得しています。
  • GetEnvironmentVariableValuesAsync 呼び出し時に DistributedApplicationOperation.Publish を渡し、バインディング式としてリソースに公開(Publish)される環境変数を対象にしています。
  • 返却された環境変数を用いて、テストでは webfrontend リソースに、apiservice リソースへ解決される HTTPS の環境変数が存在することを検証しています。

Aspire ソリューションのテストを記述する際、デバッグやテスト実行状況の確認のためにログを取得・表示したい場面があるかと思います。DistributedApplicationTestingBuilder ではサービス コレクションにアクセスできるため、テスト シナリオ向けにログ設定を行うことができます。

テストからログを取得するには、builder.Services に対して AddLogging メソッドを使用し、テスト フレームワーク向けのログ プロバイダーを構成します:

LoggingTest.cs
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);
}
}
LoggingTest.cs
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);
}
}
LoggingTest.cs
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));
}
}

テスト プロジェクトでは、アプリケーション側の 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 では、次のようなログ パッケージの利用が考えられます:

MSTest では、次のようなログ パッケージの利用が考えられます:

NUnit では、次のようなログ パッケージの利用が考えられます:

Aspire のテスト プロジェクト テンプレートを使うことで、Aspire ソリューション向けのテスト プロジェクトを簡単に作成できます。テンプレートにはサンプル テストが含まれており、それを出発点として独自のテストを作成できます。DistributedApplicationTestingBuilder は、ASP.NET Core における WebApplicationFactory<T> とよく似たパターンに従って設計されています。分散アプリケーション用のテスト ホストを作成し、それに対してテストを実行できる点が特長です。

また、DistributedApplicationTestingBuilder を使用すると、すべてのリソース ログは既定で DistributedApplication にリダイレクトされます。この仕組みにより、特定のリソースが正しくログを出力しているかどうかをテストで検証する、といったシナリオも実現できます。

質問 & 回答コラボレーションコミュニティディスカッション視聴