ヒント
このコンテンツは、ASP.NET Core と Azure を使用した最新の Web アプリケーションの設計に関する電子ブックからの抜粋です。 .NET Docs またはオフラインで読み取ることができる無料のダウンロード可能な PDF として入手できます。
「製品の単体テストが気に入らない場合は、ほとんどの場合、顧客もテストを好まないでしょう」。 _-アノニマス-
変更に対応するとき、複雑なソフトウェアには予想外のエラーが発生することがあります。 そのため、ほとんどの些細な (少なくとも重要性が最も低い) アプリケーションを除くすべてのアプリケーションで変更後のテストが必須となります。 手動テストはソフトウェアのテスト方法として最も遅く、信頼性がなく、高額です。 残念ながら、アプリケーションがテスト可能に設計されていない場合は、それが使用可能なテストの唯一の手段になる可能性があります。 第 4 章で説明したアーキテクチャの原則に従って記述されたアプリケーションは、主に単体テスト可能である必要があります。 ASP.NET Core アプリケーションは、自動化された統合と機能テストをサポートします。
自動テストの種類
ソフトウェア アプリケーションでは、さまざまな種類のテストが自動化されています。 最も単純でレベルの低いテストが単体テストです。 少し高いレベルに、統合テストや機能テストがあります。 UI テスト、ロード テスト、ストレス テスト、スモーク テストなど、他の種類のテストは、このドキュメントの範囲外です。
単体テスト
単体テストでは、アプリケーションのロジックの 1 つの部分をテストします。 単体テストに含まれない内容を列挙するして単体テストをさらに説明できます。 単体テストでは、依存関係やインフラストラクチャとのコードの連動をテストしません。それは統合テストの対象です。 単体テストでは、コードが記述されているフレームワークはテストされません。動作すると想定するか、そうでない場合はバグを報告し、回避策をコーディングする必要があります。 単体テストは、メモリ内とプロセス内で完全に実行されます。 ファイル システム、ネットワーク、またはデータベースと通信しません。 単体テストでは、コードのみをテストする必要があります。
単体テストは、外部の依存関係を持たないコードの 1 つのユニットのみをテストするため、非常に高速に実行する必要があります。 したがって、数百の単体テストのテスト スイートを数秒で実行できる必要があります。 共有ソース管理リポジトリへのすべてのプッシュの前に、ビルド サーバー上のすべての自動ビルドを使用して、それらを頻繁に実行するのが理想的です。
統合テスト
データベースやファイル システムなど、インフラストラクチャとやり取りするコードをカプセル化することは良い考えですが、それでもカプセル化されないコードが残るので、それをテストすることになります。 また、アプリケーションの依存関係が完全に解決されるとき、コードの層が予想どおりにやり取りすることを検証してください。 この機能は統合テストの担当です。 統合テストは、外部の依存関係とインフラストラクチャに依存することが多いため、単体テストよりも遅く、セットアップが困難になる傾向があります。 そのため、統合テストで単体テストでテストできるテストは避ける必要があります。 特定のシナリオを単体テストでテストできる場合は、単体テストでテストする必要があります。 できない場合は、統合テストの使用を検討してください。
多くの場合、統合テストには、単体テストよりも複雑なセットアップと破棄の手順があります。 たとえば、実際のデータベースに対して実行される統合テストでは、各テストを実行する前に、データベースを既知の状態に戻す方法が必要です。 新しいテストが追加され、運用データベース スキーマが進化すると、これらのテスト スクリプトのサイズと複雑さが増す傾向があります。 多くの大規模なシステムでは、共有ソース管理への変更をチェックインする前に、開発者ワークステーションで統合テストの完全なスイートを実行することは現実的ではありません。 このような場合は、ビルド サーバーで統合テストを実行できます。
機能テスト
統合テストは、システムの一部のコンポーネントが正常に連携していることを確認するために、開発者の観点から記述されます。 機能テストは、ユーザーの観点から記述され、その要件に基づいてシステムの正確性を検証します。 次の抜粋は、単体テストと比較して、機能テストについて考える方法の便利な例を示しています。
「多くの場合、システムの開発は家の建物に似ています。 この例えはまったく正しくありませんが、単体テストと機能テストの違いを理解するために拡張できます。 単体テストは、住宅の建設現場を訪れる建築検査官に似ています。 彼は家の様々な内部システム、基礎、フレーム、電気、配管に焦点を当てています。 彼は、家の一部が正しく安全に動作すること、つまり建物のコードを満たしていることを確認 (テスト) します。 このシナリオの機能テストは、この同じ建設現場を訪れる住宅所有者に似ています。 彼は、内部システムが適切に動作し、建物検査官が自分のタスクを実行していることを前提としています。 住宅所有者は、この家に住むのがどんなものに焦点を当てています。 彼は家がどのように見えるかに関心があり、様々な部屋が快適なサイズかどうか、家が家族のニーズに合っているか、朝の光をうまく取り入れるために窓が良い位置にあるかを考慮しています。 住宅所有者は、家で機能テストを実行しています。 彼はユーザーの視点を持っています。 建物検査官は、家の単体テストを実行しています。 彼はビルダーの視点を持っています。
ソース: 単体テストと機能テスト
私は「開発者として、私たちは2つの方法で失敗する:私たちは間違ったものを構築するか、間違ったものを構築する」と言うのが好きです。単体テストでは、正しいものを構築していることを確認します。機能テストでは、正しいものを構築していることを確認します。
機能テストはシステム レベルで動作するため、ある程度の UI オートメーションが必要になる場合があります。 統合テストと同様に、通常は何らかのテスト インフラストラクチャでも動作します。 このアクティビティにより、単体テストや統合テストよりも低速で脆弱になります。 機能テストは、システムがユーザーが期待するとおりに動作することを確信する必要がある数だけ必要です。
ピラミッドのテスト
Martin Fowler はテスト ピラミッドについて書きました。その例を図 9-1 に示します。
図 9-1 ピラミッドのテスト
ピラミッドのさまざまなレイヤーとその相対サイズは、さまざまな種類のテストと、アプリケーションに対して記述する必要がある数を表します。 ご覧のように、より小さな統合テストのレイヤーでサポートされる単体テストのベースが大きく、機能テストのレイヤーがさらに小さいようにすることをお勧めします。 各レイヤーには、下位レイヤーで適切に実行できないテストのみが理想的です。 特定のシナリオに必要なテストの種類を決定する場合は、テスト ピラミッドを念頭に置いておきます。
何をテストするか
自動テストの記述に慣れていない開発者にとって一般的な問題は、どのような内容をテストすべきかを見極めることです。 良い出発点は、条件付きロジックをテストすることです。 条件付きステートメント (if-else、switch など) に基づいて動作を変更するメソッドがある場合は、特定の条件に対して正しい動作を確認するテストを少なくとも 2 つ考え出せるはずです。 コードにエラー条件がある場合は、コードを通じて "happy path" のテストを少なくとも 1 つ記述し (エラーなし)、"悲しいパス" のテストを少なくとも 1 つ (エラーまたは非定型の結果を含む) を記述して、エラーが発生した場合にアプリケーションが期待どおりに動作することを確認することをお勧めします。 最後に、コード カバレッジなどのメトリックに焦点を当てるのではなく、失敗する可能性のあるテストに焦点を当ててみてください。 一般的に、カバレッジは少ないよりも多い方が良いとされます。 ただし、複雑でビジネスクリティカルなメソッドのいくつかのテストを記述することは、通常、テスト コード カバレッジ メトリックを改善するために自動プロパティのテストを記述するよりも、時間を使う方が適しています。
テスト プロジェクトの整理
テストプロジェクトは自分にとって最適な方法で整理できます。 テストは、種類 (単体テスト、統合テスト) とテスト対象 (プロジェクト別、名前空間別) で分離することをお勧めします。 この分離が 1 つのテスト プロジェクト内のフォルダーで構成されているか、複数のテスト プロジェクトで構成されるかは、設計上の決定です。 1 つのプロジェクトは最も簡単ですが、テストが多い大規模なプロジェクトの場合や、さまざまなテスト セットをより簡単に実行するには、複数の異なるテスト プロジェクトが必要になる場合があります。 多くのチームは、テスト対象のプロジェクトに基づいてテスト プロジェクトを編成します。複数のプロジェクトを含むアプリケーションの場合、特に、各プロジェクトのテストの種類に応じてこれらを分割する場合は特に、多数のテスト プロジェクトが発生する可能性があります。 セキュリティ侵害のアプローチでは、テストの種類ごとに 1 つのプロジェクトを、アプリケーションごとに、テスト プロジェクト内のフォルダーと共に、テスト対象のプロジェクト (およびクラス) を示します。
一般的な方法は、アプリケーション プロジェクトを 'src' フォルダーの下に整理し、アプリケーションのテスト プロジェクトを並列の 'tests' フォルダーの下に整理することです。 この組織が役立つ場合は、Visual Studio で一致するソリューション フォルダーを作成できます。
図 9-2 ソリューション内のテスト組織
任意のテスト フレームワークを使用できます。 xUnit フレームワークは適切に機能し、ASP.NET Core テストと EF Core テストがすべて記述されている内容です。 図 9-3 に示すテンプレートを使用して、または dotnet new xunit を使用して CLI から、Visual Studio で xUnit テスト プロジェクトを追加できます。
図 9-3 Visual Studio で xUnit テスト プロジェクトを追加する
名前付けのテスト
テストに一貫性のある名前を付け、各テストの動作を示す名前を付けます。 私が大きな成功を収めたアプローチの1つは、テストしているクラスとメソッドに従ってテストクラスに名前を付けることです。 この方法では、多数の小さなテスト クラスが作成されますが、各テストの役割が非常に明確になります。 テスト クラス名を設定すると、テスト対象のクラスとメソッドを識別するために、テスト メソッド名を使用して、テスト対象の動作を指定できます。 この名前には、予期される動作と、この動作を生成する必要がある入力または前提条件が含まれている必要があります。 テスト名の例を次に示します。
CatalogControllerGetImage.CallsImageServiceWithIdCatalogControllerGetImage.LogsWarningGivenImageMissingExceptionCatalogControllerGetImage.ReturnsFileResultWithBytesGivenSuccessCatalogControllerGetImage.ReturnsNotFoundResultGivenImageMissingException
この方法のバリエーションにより、各テスト クラス名が "Should" で終わり、時制が少し変更されます。
CatalogControllerGetImageは.ImageServiceWithIdを呼び出すべきですCatalogControllerGetImageは.WarningGivenImageMissingExceptionをログに記録すべきです
一部のチームでは、2つ目の名前付け方法がやや冗長ですが、より明確であると感じています。 いずれの場合も、テスト動作に関する分析情報を提供する名前付け規則を使用して、1 つ以上のテストが失敗したときに、失敗したケースの名前から明らかになるようにしてください。 ControllerTests.Test1 など、テストにあいまいな名前を付けないでください。これらの名前はテスト結果に表示されるときに値を提供しないためです。
多数の小さなテスト クラスを生成する上記のような名前付け規則に従う場合は、フォルダーと名前空間を使用してテストをさらに整理することをお勧めします。 図 9-4 は、複数のテスト プロジェクト内のフォルダーごとにテストを整理する 1 つの方法を示しています。
図 9-4 テスト対象のクラスに基づいて、フォルダー別にテスト クラスを整理する。
特定のアプリケーション クラスにテスト対象のメソッドが多数ある場合 (そのため、多くのテスト クラス)、アプリケーション クラスに対応するフォルダーにこれらのクラスを配置することが理にかなっている可能性があります。 この組織は、ファイルを他の場所のフォルダーに整理する方法と同じです。 他の多くのファイルを含むフォルダーに 3 つまたは 4 つ以上の関連ファイルがある場合は、多くの場合、それらを独自のサブフォルダーに移動すると便利です。
コア アプリ ASP.NET 単体テスト
適切に設計された ASP.NET Core アプリケーションでは、複雑さとビジネス ロジックのほとんどは、ビジネス エンティティとさまざまなサービスにカプセル化されます。 コントローラー、フィルター、ビューモデル、ビューを含む ASP.NET Core MVC アプリ自体には、単体テストはほとんど必要ありません。 特定のアクションの機能の多くは、アクション メソッド自体の外部にあります。 ルーティングまたはグローバル エラー処理が正しく機能するかどうかをテストすることは、単体テストでは効果的に実行できません。 同様に、モデルの検証と認証、承認のフィルターを含むフィルターは、コントローラーのアクション メソッドを対象とするテストでは単体テストできません。 これらの動作のソースがない場合、ほとんどのアクション メソッドは単純に小さく、それらの作業の大部分を、それらを使用するコントローラーから独立してテストできるサービスに委任する必要があります。
場合によっては、コードを単体テストするためにコードをリファクタリングする必要があります。 多くの場合、このアクティビティには、インフラストラクチャに対して直接コーディングするのではなく、抽象化を識別し、依存関係の挿入を使用して、テストするコードの抽象化にアクセスすることが含まれます。 たとえば、画像を表示するための次の簡単なアクション方法を考えてみましょう。
[HttpGet("[controller]/pic/{id}")]
public IActionResult GetImage(int id)
{
var contentRoot = _env.ContentRootPath + "//Pics";
var path = Path.Combine(contentRoot, id + ".png");
Byte[] b = System.IO.File.ReadAllBytes(path);
return File(b, "image/png");
}
このメソッドの単体テストは、ファイル システムからの読み取りに使用する System.IO.Fileへの直接の依存関係によって困難になります。 この動作をテストして期待どおりに動作することを確認できますが、実際のファイルで行うことは統合テストです。 このメソッドのルートを単体テストすることはできません。機能テストを使用してこのテストを実行する方法については、間もなく説明します。
ファイル システムの動作を直接単体テストできない場合、ルートをテストできない場合は、何をテストする必要がありますか? さて、単体テストを可能にするためにリファクタリングした後、いくつかのテスト ケースや、エラー処理などの動作が見つからない場合があります。 ファイルが見つからない場合、このメソッドは何をしますか? どのような操作を行う必要がありますか? この例では、リファクタリングされたメソッドは次のようになります。
[HttpGet("[controller]/pic/{id}")]
public IActionResult GetImage(int id)
{
byte[] imageBytes;
try
{
imageBytes = _imageService.GetImageBytesById(id);
}
catch (CatalogImageMissingException ex)
{
_logger.LogWarning($"No image found for id: {id}");
return NotFound();
}
return File(imageBytes, "image/png");
}
_logger と _imageService の両方が依存関係として挿入されます。 これで、アクション メソッドに渡されたのと同じ ID が _imageServiceに渡され、結果のバイトが FileResult の一部として返されることをテストできます。 また、エラー ログが予期したとおりに発生していること、およびイメージが見つからない場合に NotFound 結果が返されることをテストすることもできます。これは、この動作が重要なアプリケーションの動作であると仮定します (つまり、開発者が問題を診断するために追加した一時的なコードだけではありません)。 実際のファイル ロジックは別の実装サービスに移行され、不足しているファイルの場合のアプリケーション固有の例外を返すように拡張されています。 統合テストを使用して、この実装を個別にテストできます。
ほとんどの場合、コントローラーでグローバル例外ハンドラーを使用する必要があるため、それらのロジックの量は最小限に抑え、単体テストの価値はないでしょう。 コントローラー アクションのテストのほとんどは、以下で説明する機能テストと TestServer クラスを使用して行います。
コア アプリ ASP.NET 統合テスト
ASP.NET Core アプリの統合テストのほとんどは、インフラストラクチャ プロジェクトで定義されているサービスやその他の実装の種類をテストする必要があります。 たとえば、 EF Core が正常に更新され、 インフラストラクチャ プロジェクトに存在するデータ アクセス クラスから期待されるデータを取得していることをテストできます。 ASP.NET Core MVC プロジェクトが正しく動作していることをテストする最善の方法は、テスト ホストで実行されているアプリに対して実行される機能テストです。
コア アプリ ASP.NET 機能テスト
ASP.NET Core アプリケーションの場合、 TestServer クラスを使用すると、機能テストを非常に簡単に記述できます。 通常、アプリケーションに対して行うように、TestServerをWebHostBuilder (またはHostBuilder)を使用して直接構成するか、バージョン2.1以降で使用可能なWebApplicationFactoryタイプを使用して構成します。 テスト ホストを可能な限り近い方法で運用ホストと照合して、運用環境でアプリが実行するのと同様の動作をテストで実行するようにします。
WebApplicationFactory クラスは、TestServer の ContentRoot を構成するのに役立ちます。これは、ビューなどの静的リソースを見つけるために ASP.NET Core によって使用されます。
単純な機能テストを作成するには、web アプリケーションのIClassFixture<WebApplicationFactory<TEntryPoint>> クラスであるTEntryPointを実装するテスト クラスStartup作成します。 このインターフェイスを配置すると、テスト フィクスチャはファクトリの CreateClient メソッドを使用してクライアントを作成できます。
public class BasicWebTests : IClassFixture<WebApplicationFactory<Program>>
{
protected readonly HttpClient _client;
public BasicWebTests(WebApplicationFactory<Program> factory)
{
_client = factory.CreateClient();
}
// write tests that use _client
}
ヒント
Program.cs ファイルで最小限の API 構成を使用している場合、既定ではクラスは内部的に宣言され、テスト プロジェクトからはアクセスできません。 代わりに、Web プロジェクト内の他のインスタンス クラスを選択するか、 Program.cs ファイルに追加できます。
// Make the implicit Program class public so test projects can access it
public partial class Program { }
多くの場合、インメモリ データ ストアを使用するようにアプリケーションを構成し、テスト データを使用してアプリケーションをシード処理するなど、各テストの実行前にサイトの追加構成を実行する必要があります。 この機能を実現するには、 WebApplicationFactory<TEntryPoint> の独自のサブクラスを作成し、その ConfigureWebHost メソッドをオーバーライドします。 次の例は eShopOnWeb FunctionalTests プロジェクトのものであり、メイン Web アプリケーションのテストの一部として使用されます。
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.eShopWeb.Infrastructure.Data;
using Microsoft.eShopWeb.Infrastructure.Identity;
using Microsoft.eShopWeb.Web;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
namespace Microsoft.eShopWeb.FunctionalTests.Web;
public class WebTestFixture : WebApplicationFactory<Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseEnvironment("Testing");
builder.ConfigureServices(services =>
{
services.AddEntityFrameworkInMemoryDatabase();
// Create a new service provider.
var provider = services
.AddEntityFrameworkInMemoryDatabase()
.BuildServiceProvider();
// Add a database context (ApplicationDbContext) using an in-memory
// database for testing.
services.AddDbContext<CatalogContext>(options =>
{
options.UseInMemoryDatabase("InMemoryDbForTesting");
options.UseInternalServiceProvider(provider);
});
services.AddDbContext<AppIdentityDbContext>(options =>
{
options.UseInMemoryDatabase("Identity");
options.UseInternalServiceProvider(provider);
});
// Build the service provider.
var sp = services.BuildServiceProvider();
// Create a scope to obtain a reference to the database
// context (ApplicationDbContext).
using (var scope = sp.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var db = scopedServices.GetRequiredService<CatalogContext>();
var loggerFactory = scopedServices.GetRequiredService<ILoggerFactory>();
var logger = scopedServices
.GetRequiredService<ILogger<WebTestFixture>>();
// Ensure the database is created.
db.Database.EnsureCreated();
try
{
// Seed the database with test data.
CatalogContextSeed.SeedAsync(db, loggerFactory).Wait();
// seed sample user data
var userManager = scopedServices.GetRequiredService<UserManager<ApplicationUser>>();
var roleManager = scopedServices.GetRequiredService<RoleManager<IdentityRole>>();
AppIdentityDbContextSeed.SeedAsync(userManager, roleManager).Wait();
}
catch (Exception ex)
{
logger.LogError(ex, $"An error occurred seeding the " +
"database with test messages. Error: {ex.Message}");
}
}
});
}
}
テストでは、このカスタム WebApplicationFactory を使用してクライアントを作成し、このクライアント インスタンスを使用してアプリケーションに要求を行うことができます。 アプリケーションには、テストのアサーションの一部として使用できるデータがシードされます。 次のテストでは、eShopOnWeb アプリケーションのホーム ページが正しく読み込まれ、シード データの一部としてアプリケーションに追加された製品の一覧が含まれていることを確認します。
using Microsoft.eShopWeb.FunctionalTests.Web;
using System.Net.Http;
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.eShopWeb.FunctionalTests.WebRazorPages;
[Collection("Sequential")]
public class HomePageOnGet : IClassFixture<WebTestFixture>
{
public HomePageOnGet(WebTestFixture factory)
{
Client = factory.CreateClient();
}
public HttpClient Client { get; }
[Fact]
public async Task ReturnsHomePageWithProductListing()
{
// Arrange & Act
var response = await Client.GetAsync("/");
response.EnsureSuccessStatusCode();
var stringResponse = await response.Content.ReadAsStringAsync();
// Assert
Assert.Contains(".NET Bot Black Sweatshirt", stringResponse);
}
}
この機能テストでは、すべてのミドルウェア、フィルター、バインダーを含む、ASP.NET Core MVC/Razor Pages アプリケーション スタック全体が実行されます。 特定のルート ("/") が予想される成功状態コードと HTML 出力を返していることを確認します。 これは、実際の Web サーバーを設定せずに行い、テストに実際の Web サーバーを使用すると発生する可能性がある脆弱性の多くを回避します (ファイアウォール設定の問題など)。 TestServer に対して実行される機能テストは、通常、統合テストや単体テストよりも低速ですが、テスト Web サーバーに対してネットワーク経由で実行されるテストよりもはるかに高速です。 機能テストを使用して、アプリケーションのフロントエンド スタックが期待どおりに動作していることを確認します。 これらのテストは、コントローラーまたはページで重複が見つかるときや、フィルターを追加して重複に対処する場合に特に便利です。 理想的には、このリファクタリングはアプリケーションの動作を変更せず、一連の機能テストでこれが当てはまることを確認します。
参照 – コア MVC アプリ ASP.NET テストする
- ASP.NET Core でのテスト
https://learn.microsoft.com/aspnet/core/testing/- 単体テストの名前付け規則
https://ardalis.com/unit-test-naming-convention- EF Core のテスト
https://learn.microsoft.com/ef/core/miscellaneous/testing/- ASP.NET Core での統合テスト
https://learn.microsoft.com/aspnet/core/test/integration-tests
.NET