ASP.NET Core での統合テスト
作成者: Jos van der Til、Martin Costello、Javier Calvarro Nelson。
統合テストでは、データベース、ファイル システム、ネットワークなど、アプリのサポート インフラストラクチャを含むレベルで、アプリのコンポーネントが正しく機能していることを確認します。 ASP.NET Core では、単体テスト フレームワークとテスト Web ホストおよびメモリ内テスト サーバーを使用した統合テストがサポートされています。
この記事を読むには、単体テストの基本的な知識があることが前提となります。 テストの概念に慣れていない場合は、.NET Core と .NET Standard の単体テストに関する記事とそのリンクされたコンテンツを参照してください。
サンプル コードを表示またはダウンロードします (ダウンロード方法)。
このサンプル アプリは Razor Pages アプリであり、Razor Pages の基本を理解していることを前提としています。 Razor Pages に慣れていない場合は、次の記事を参照してください。
SPA をテストする場合は、ブラウザーを自動化できる Playwright for .NET などのツールをお勧めします。
統合テストの概要
統合テストでは 単体テストよりも広範なレベルでアプリのコンポーネントを評価します。 単体テストは、個々のクラス メソッドなど、分離されたソフトウェア コンポーネントをテストするために使用されます。 統合テストでは、2 つ以上のアプリ コンポーネントが、連携して予想される結果を生成することを確認します。これは、要求を完全に処理するために必要なすべてのコンポーネントを含む可能性があります。
これらの広範なテストは、アプリのインフラストラクチャとフレームワーク全体をテストするために使用されます。多くの場合、次のコンポーネントが含まれます。
- データベース
- ファイル システム
- ネットワーク アプライアンス
- 要求 - 応答パイプライン
単体テストでは、インフラストラクチャ コンポーネントの代わりに、フェイクまたはモック オブジェクトと呼ばれる、作成済みのコンポーネントを使用します。
単体テストと比較すると、統合テストは次のようになります。
- アプリが運用環境で使用する実際のコンポーネントを使用します。
- より多くのコードとデータ処理が必要です。
- 実行に時間がかかります。
そのため、統合テストの使用は最も重要なインフラストラクチャ シナリオに限定します。 単体テストと統合テストのどちらを使用しても動作をテストできる場合は、単体テストを選択します。
統合テストの説明では、テスト対象のプロジェクトのことをよく "テスト対象システム"、または短縮して "SUT" と呼びます。 この記事全体を通して、テスト対象の ASP.NET Core アプリを指すために "SUT" を使用します。
データベースとファイル システムを使用するデータおよびファイル アクセスの "すべての順列として統合テストを記述してはいけません"。 通常は、アプリ全体でデータベースやファイル システムを操作する場所がいくつあったとしても、的を絞った一連の読み取り、書き込み、更新、削除の統合テストを行うことで、データベースおよびファイル システム コンポーネントを適切にテストすることができます。 これらのコンポーネントと連携するメソッドのロジックのルーチン テストには、単体テストを使用します。 単体テストでは、インフラストラクチャのフェイクまたはモックを使用することにより、テストの実行時間が短縮されます。
ASP.NET Core 統合テスト
ASP.NET Core の統合テストには、次のものが必要です。
- テスト プロジェクトは、テストを格納して実行するために使用します。 テスト プロジェクトは SUT への参照を含みます。
- テスト プロジェクトは、SUT のテスト Web ホストを作成し、テスト サーバー クライアントを使用して SUT との要求と応答を処理します。
- テスト ランナーは、テストを実行し、テスト結果を報告するために使用されます。
統合テストでは、通常の Arrange (配置) 、Act (実行) 、および Assert (確認) のテスト ステップを含む一連のイベントに従います。
- SUT の Web ホストが構成されます。
- アプリに要求を送信するためのテスト サーバー クライアントが作成されます。
- Arrange (配置) テスト ステップが実行されます。テスト アプリが要求を準備します。
- Act (実行) テスト ステップが実行されます。クライアントは要求を送信し、応答を受信します。
- Assert (確認) テスト ステップが実行されます。実際の応答は、予測される応答に基づき、成功または失敗として検証されます。
- このプロセスは、すべてのテストが実行されるまで続行されます。
- テスト結果が報告されます。
通常、テスト Web ホストは、アプリの通常のテスト用の Web ホストとは異なる方法で構成されています。 たとえば、テスト用に別のデータベースまたは異なるアプリ設定を使用する場合があります。
テスト Web ホストやメモリ内テスト サーバー (TestServer) などのインフラストラクチャ コンポーネントは、Microsoft.AspNetCore.Mvc.Testing パッケージによって提供または管理されます。 このパッケージを使用すると、テストの作成と実行を効率化できます。
Microsoft.AspNetCore.Mvc.Testing
パッケージは、次のタスクを処理します。
- 依存関係ファイル (
.deps
) を SUT からテスト プロジェクトのbin
ディレクトリにコピーします。 - テストを実行したときに、静的なファイルとページ/ビューが検出されるように、コンテンツ ルートを SUT のプロジェクト ルートに設定します。
- WebApplicationFactory クラスを提供し、
TestServer
を使用して SUT のブートストラップを効率化します。
単体テストのドキュメントでは、テスト プロジェクトとテスト ランナーを設定する方法、テストを実行する方法の詳細な手順、テストおよびテスト クラスの命名方法に関する推奨事項について説明します。
単体テストを統合テストから分離し、異なるプロジェクトにします。 テストの分離は、次の面で役立ちます。
- インフラストラクチャ テスト コンポーネントが誤って単体テストに含まれないようにすることができる。
- 実行されるテストのセットを制御することができる。
Razor Pages アプリと MVC アプリのテストの構成には、ほぼ違いがありません。 唯一の違いは、テストの命名方法です。 Razor Pages アプリでは、ページ エンドポイントのテストは通常、ページ モデル クラスにちなんだ名前が付けられます (たとえば、IndexPageTests
では Index ページのコンポーネントの統合テストが行われます)。 MVC アプリでは、テストは通常、コントローラー クラス別に編成され、テストするコントローラーにちなんだ名前が付けられます (たとえば、HomeControllerTests
は Home コントローラーのコンポーネントの統合テストを行います)。
テスト アプリの前提条件
テスト プロジェクトは、次の条件を満たす必要があります。
Microsoft.AspNetCore.Mvc.Testing
パッケージを参照します。- プロジェクト ファイル (
<Project Sdk="Microsoft.NET.Sdk.Web">
) で Web SDK を指定しています。
これらの前提条件は、サンプル アプリで確認できます。 tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj
ファイルを確認します。 このサンプル アプリでは、xUnit テスト フレームワークと AngleSharp パーサー ライブラリを使用するので、サンプル アプリは以下も参照します。
xunit.runner.visualstudio
バージョン 2.4.2 以降を使用するアプリでは、テスト プロジェクトによって Microsoft.NET.Test.Sdk
パッケージが参照される必要があります。
テストでは Entity Framework Core も使用します。 GitHub のプロジェクト ファイルを参照してください。
SUT 環境
SUT の 環境 が設定されていない場合、環境は既定で開発になります。
既定の WebApplicationFactory を使用した基本的なテスト
次のいずれかの操作を行って、暗黙的に定義された Program
クラスをテスト プロジェクトに公開します。
Web アプリからテスト プロジェクトに内部型を公開します。 これは、SUT プロジェクトのファイル (
.csproj
) 内で実行できます。<ItemGroup> <InternalsVisibleTo Include="MyTestProject" /> </ItemGroup>
部分クラス宣言を使用して、
Program
クラスをパブリックにします。var builder = WebApplication.CreateBuilder(args); // ... Configure services, routes, etc. app.Run(); + public partial class Program { }
サンプル アプリでは、
Program
部分クラスの方法を使用しています。
WebApplicationFactory<TEntryPoint> は、統合テスト用の TestServer を作成するために使用します。 TEntryPoint
は SUT のエントリ ポイント クラスであり、通常は Program.cs
です。
テスト クラスでは、クラスにテストが含まれていることを示すために "クラス フィクスチャ" インターフェイス (IClassFixture
) を実装し、クラス内のテストの共有オブジェクト インスタンスを提供します。
次のテスト クラス BasicTests
では、WebApplicationFactory
を使用して SUT をブートストラップし、テスト メソッド Get_EndpointsReturnSuccessAndCorrectContentType
に HttpClient を提供します。 このメソッドは、複数のアプリ ページで応答状態コードが成功かどうか (200-299) と、Content-Type
ヘッダーが text/html; charset=utf-8
であるかどうかを確認します。
CreateClient() では、自動的にリダイレクトに従い、Cookie を処理する、HttpClient
のインスタンスを作成します。
public class BasicTests
: IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public BasicTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
}
[Theory]
[InlineData("/")]
[InlineData("/Index")]
[InlineData("/About")]
[InlineData("/Privacy")]
[InlineData("/Contact")]
public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
{
// Arrange
var client = _factory.CreateClient();
// Act
var response = await client.GetAsync(url);
// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.Equal("text/html; charset=utf-8",
response.Content.Headers.ContentType.ToString());
}
}
既定では、一般データ保護規則同意ポリシーが有効になっている場合、要求間で必須でない Cookie は保持されません。 TempData プロバイダーで使用されているような必須ではない Cookie を保持するには、テストに必須であることをマークします。 cookie を必須としてマークする手順については、必須 Cookie をご覧ください。
偽造防止チェックのための AngleSharp と Application Parts
この記事では、AngleSharp パーサーを使い、ページを読み込んで HTML を解析することで、偽造防止チェックを処理します。 コントローラーと Razor Pages ビューのエンドポイントを低レベルで、ブラウザーでどのようにレンダリングされるかを気にせずテストする場合は、Application Parts
の使用を検討してください。 Application Parts の方法では、コントローラーまたは Razor Page をアプリに挿入します。これは必要な値を取得する JSON 要求を行うために使用できます。 詳細については、Martin Costello によるブログ「Application Parts を使用した偽造防止によって保護される ASP.NET Core リソースの統合テスト」と関連する GitHub リポジトリを参照してください。
WebApplicationFactory のカスタマイズ
Web ホストの構成は、WebApplicationFactory<TEntryPoint> から継承して 1 つ以上のカスタム ファクトリを作成することで、テスト クラスとは別に作成できます。
WebApplicationFactory
から継承し、ConfigureWebHost をオーバーライドします。 IWebHostBuilder では、IWebHostBuilder.ConfigureServices
を使用してサービス コレクションを構成できます。public class CustomWebApplicationFactory<TProgram> : WebApplicationFactory<TProgram> where TProgram : class { protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureServices(services => { var dbContextDescriptor = services.SingleOrDefault( d => d.ServiceType == typeof(DbContextOptions<ApplicationDbContext>)); services.Remove(dbContextDescriptor); var dbConnectionDescriptor = services.SingleOrDefault( d => d.ServiceType == typeof(DbConnection)); services.Remove(dbConnectionDescriptor); // Create open SqliteConnection so EF won't automatically close it. services.AddSingleton<DbConnection>(container => { var connection = new SqliteConnection("DataSource=:memory:"); connection.Open(); return connection; }); services.AddDbContext<ApplicationDbContext>((container, options) => { var connection = container.GetRequiredService<DbConnection>(); options.UseSqlite(connection); }); }); builder.UseEnvironment("Development"); } }
サンプル アプリでのデータベースのシード処理は、
InitializeDbForTests
メソッドによって実行されます。 このメソッドについては、統合テストのサンプル: テスト アプリの構成に関するセクションをご覧ください。SUT のデータベース コンテキストは、
Program.cs
に登録されます。 テスト アプリのbuilder.ConfigureServices
コールバックは、アプリのProgram.cs
コードが実行された後に実行されます。 アプリのデータベースとは異なるデータベースをテストに使用するには、builder.ConfigureServices
でアプリのデータベース コンテキストを置き換える必要があります。サンプル アプリでは、データベース コンテキストのサービス記述子を検索し、記述子を使用してサービス登録を削除しています。 次に、ファクトリは、テストにメモリ内データベースを使用する新しい
ApplicationDbContext
を追加します。別のデータベースに接続するには、
DbConnection
を変更します。 SQL Server テスト データベースを使用するには、次のようにします。- プロジェクト ファイルで
Microsoft.EntityFrameworkCore.SqlServer
NuGet パッケージを参照します。 UseInMemoryDatabase
を呼び出します。
- プロジェクト ファイルで
テスト クラスでカスタム
CustomWebApplicationFactory
を使用します。 次の例では、IndexPageTests
クラスでファクトリを使用しています。public class IndexPageTests : IClassFixture<CustomWebApplicationFactory<Program>> { private readonly HttpClient _client; private readonly CustomWebApplicationFactory<Program> _factory; public IndexPageTests( CustomWebApplicationFactory<Program> factory) { _factory = factory; _client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); }
サンプル アプリのクライアントは、
HttpClient
がリダイレクトに従わないように構成されています。 モック認証のセクションで後述するように、これによってアプリの最初の応答結果を確認するテストが可能になります。 これらのテストでは、多くの場合、最初の応答はLocation
ヘッダーを持つリダイレクトです。一般的なテストでは、
HttpClient
およびヘルパー メソッドを使用して、要求と応答を処理します。[Fact] public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot() { // Arrange var defaultPage = await _client.GetAsync("/"); var content = await HtmlHelpers.GetDocumentAsync(defaultPage); //Act var response = await _client.SendAsync( (IHtmlFormElement)content.QuerySelector("form[id='messages']"), (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']")); // Assert Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode); Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); Assert.Equal("/", response.Headers.Location.OriginalString); }
SUT に対する POST 要求は、アプリの偽造防止データ保護システムによって自動的に行われる偽造防止チェックを満たす必要があります。 テストで POST 要求を実行するには、テスト アプリで次のことを行う必要があります。
- ページに対して要求を行います。
- 応答の偽造防止 cookie と要求検証トークンを解析します。
- 偽造防止 cookie と要求検証トークンを使用して POST 要求を行います。
サンプル アプリの SendAsync
ヘルパー拡張メソッド (Helpers/HttpClientExtensions.cs
) と GetDocumentAsync
ヘルパー メソッド (Helpers/HtmlHelpers.cs
) は、次のメソッドで AngleSharp パーサーを使用して偽造防止チェック処理を行います。
GetDocumentAsync
: HttpResponseMessage を受け取り、IHtmlDocument
を返します。GetDocumentAsync
は、元のHttpResponseMessage
に基づいて仮想応答を準備するファクトリを使用します。 詳しくは、AngleSharp のドキュメントをご覧ください。HttpClient
のSendAsync
拡張メソッドを使って HttpRequestMessage を作成し、SendAsync(HttpRequestMessage) を呼び出して SUT に要求を送信します。SendAsync
のオーバーロードは、HTML フォーム (IHtmlFormElement
) と次のものを受け入れます。- フォームの送信ボタン (
IHtmlElement
) - フォームの値コレクション (
IEnumerable<KeyValuePair<string, string>>
) - 送信ボタン (
IHtmlElement
) とフォームの値 (IEnumerable<KeyValuePair<string, string>>
)
- フォームの送信ボタン (
AngleSharp は、この記事とサンプル アプリのデモンストレーションのために使用するサードパーティ製の解析ライブラリです。 ASP.NET Core アプリの統合テストでは、AngleSharp はサポートされていないか、必要ありません。 Html Agility Pack (HAP) などの他のパーサーを使用することもできます。 もう 1 つの方法として、偽造防止システムの要求検証トークンを処理するコードを記述し、偽造防止 cookie を直接処理する方法もあります。 詳細については、この記事の「偽造防止チェックのための AngleSharp と Application Parts
」を参照してください。
EF-Core インメモリ データベース プロバイダーは、限定された基本的なテストに使用できます。一方、"SQLite プロバイダーは、インメモリ テストに推奨される選択肢です"。
「スタートアップ フィルターを使用した Startup の拡張」をご覧ください。テストにカスタムのサービスやミドルウェアを必要とするときに便利な、IStartupFilter を使用したミドルウェアの構成方法がわかります。
WithWebHostBuilder を使用したクライアントのカスタマイズ
テスト メソッド内で追加の構成が必要な場合、WithWebHostBuilder では、IWebHostBuilder を使用して新しい WebApplicationFactory
を作成できます。
このサンプル コードは WithWebHostBuilder
を呼び出し、構成されたサービスをテスト スタブに置き換えます。 詳細と使用例については、この記事の「モック サービスの注入」を参照してください。
サンプル アプリ の Post_DeleteMessageHandler_ReturnsRedirectToRoot
テスト メソッドは、WithWebHostBuilder
の使用方法を示しています。 このテストでは、SUT からフォーム送信をトリガーすることによって、データベース内のレコードの削除を実行します。
IndexPageTests
クラス内の別のテストは、データベース内のすべてのレコードを削除する操作を実行します。この操作が Post_DeleteMessageHandler_ReturnsRedirectToRoot
メソッドの前に実行される可能性があるため、このテスト メソッド内でデータベースの再シード処理を行い、確実に SUT が削除するレコードが存在するようにしています。 SUT に対する要求では、SUT の messages
フォームの最初の [削除] ボタンの選択がシミュレートされます。
[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
// Arrange
using (var scope = _factory.Services.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var db = scopedServices.GetRequiredService<ApplicationDbContext>();
Utilities.ReinitializeDbForTests(db);
}
var defaultPage = await _client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
//Act
var response = await _client.SendAsync(
(IHtmlFormElement)content.QuerySelector("form[id='messages']"),
(IHtmlButtonElement)content.QuerySelector("form[id='messages']")
.QuerySelector("div[class='panel-body']")
.QuerySelector("button"));
// Assert
Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("/", response.Headers.Location.OriginalString);
}
クライアントのオプション
HttpClient
インスタンスを作成するときの既定値と使用可能なオプションについては、WebApplicationFactoryClientOptions に関するページを参照してください。
WebApplicationFactoryClientOptions
クラスを作成し、CreateClient() メソッドに渡します。
public class IndexPageTests :
IClassFixture<CustomWebApplicationFactory<Program>>
{
private readonly HttpClient _client;
private readonly CustomWebApplicationFactory<Program>
_factory;
public IndexPageTests(
CustomWebApplicationFactory<Program> factory)
{
_factory = factory;
_client = factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
}
"注": HTTPS リダイレクト ミドルウェアを使用する場合に、HTTPS リダイレクト警告のログを回避するには BaseAddress = new Uri("https://localhost")
を設定します
モック サービスの注入
ホスト ビルダーで ConfigureTestServices を呼び出すと、テストでサービスをオーバーライドできます。 オーバーライドされたサービスのスコープをテスト自体に設定するには、WithWebHostBuilder メソッドを使用してホスト ビルダーを取得します。 これは次のテストで確認できます。
- Get_QuoteService_ProvidesQuoteInPage
- Get_GithubProfilePageCanGetAGithubUser
- Get_SecurePageIsReturnedForAnAuthenticatedUser
サンプルの SUT には、引用符を返すスコープ サービスが含まれています。 インデックス ページが要求されると、インデックス ページの非表示フィールドに引用符が埋め込まれます。
Services/IQuoteService.cs
:
public interface IQuoteService
{
Task<string> GenerateQuote();
}
Services/QuoteService.cs
:
// Quote ©1975 BBC: The Doctor (Tom Baker); Dr. Who: Planet of Evil
// https://www.bbc.co.uk/programmes/p00pyrx6
public class QuoteService : IQuoteService
{
public Task<string> GenerateQuote()
{
return Task.FromResult<string>(
"Come on, Sarah. We've an appointment in London, " +
"and we're already 30,000 years late.");
}
}
Program.cs
:
services.AddScoped<IQuoteService, QuoteService>();
Pages/Index.cshtml.cs
:
public class IndexModel : PageModel
{
private readonly ApplicationDbContext _db;
private readonly IQuoteService _quoteService;
public IndexModel(ApplicationDbContext db, IQuoteService quoteService)
{
_db = db;
_quoteService = quoteService;
}
[BindProperty]
public Message Message { get; set; }
public IList<Message> Messages { get; private set; }
[TempData]
public string MessageAnalysisResult { get; set; }
public string Quote { get; private set; }
public async Task OnGetAsync()
{
Messages = await _db.GetMessagesAsync();
Quote = await _quoteService.GenerateQuote();
}
Pages/Index.cs
:
<input id="quote" type="hidden" value="@Model.Quote">
次のマークアップは、SUT アプリの実行時に生成されます。
<input id="quote" type="hidden" value="Come on, Sarah. We've an appointment in
London, and we're already 30,000 years late.">
統合テストでサービスと引用符の注入をテストするため、テストは SUT にモック サービスを注入します。 モック サービスは、アプリの QuoteService
をテスト アプリが提供する TestQuoteService
と呼ばれるサービスに置き換えます。
IntegrationTests.IndexPageTests.cs
:
// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
public Task<string> GenerateQuote()
{
return Task.FromResult(
"Something's interfering with time, Mr. Scarman, " +
"and time is my business.");
}
}
ConfigureTestServices
が呼び出され、スコープ サービスが登録されます。
[Fact]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddScoped<IQuoteService, TestQuoteService>();
});
})
.CreateClient();
//Act
var defaultPage = await client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
var quoteElement = content.QuerySelector("#quote");
// Assert
Assert.Equal("Something's interfering with time, Mr. Scarman, " +
"and time is my business.", quoteElement.Attributes["value"].Value);
}
TestQuoteService
によって指定された引用符テキストがテストの実行中に生成されたマークアップに反映されるため、アサーションは成功します。
<input id="quote" type="hidden" value="Something's interfering with time,
Mr. Scarman, and time is my business.">
モック認証
AuthTests
クラスのテストは、セキュリティで保護されたエンドポイントであることを確認します。
- 認証されていないユーザーは、アプリのサインイン ページにリダイレクトされます。
- 認証されたユーザーには、コンテンツを返します。
SUT の /SecurePage
ページでは、AuthorizePage 規約を使用してページに AuthorizeFilter を適用します。 詳細については、Razor Pages の承認規則に関するページを参照してください。
services.AddRazorPages(options =>
{
options.Conventions.AuthorizePage("/SecurePage");
});
Get_SecurePageRedirectsAnUnauthenticatedUser
テストでは、AllowAutoRedirect を false
に設定することで、WebApplicationFactoryClientOptions がリダイレクトを許可しないように設定しています。
[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
// Arrange
var client = _factory.CreateClient(
new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
// Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.StartsWith("http://localhost/Identity/Account/Login",
response.Headers.Location.OriginalString);
}
クライアントがリダイレクトに従うことを許可しないことで、次のチェックを行うことができます。
- SUT によって返される状態コードを確認するには、サインイン ページにリダイレクトした後の最終的な状態コード (これは HttpStatusCode.OK になります) ではなく、予期される HttpStatusCode.Redirect の結果と照らし合わせます。
- 最終的なサインイン ページ応答 (ここでは、
Location
ヘッダーは存在しません) ではなく、応答ヘッダーのLocation
ヘッダー値がhttp://localhost/Identity/Account/Login
で始まることを確認できます。
テスト アプリでは、認証と承認の側面をテストするために ConfigureTestServices の AuthenticationHandler<TOptions> をモックすることができます。 最小のシナリオでは、AuthenticateResult.Success が返されます。
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder)
: base(options, logger, encoder)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "TestScheme");
var result = AuthenticateResult.Success(ticket);
return Task.FromResult(result);
}
}
TestAuthHandler
は、認証スキームが TestScheme
に設定されている場合 (この場合、AddAuthentication
が ConfigureTestServices
に登録されています) に、ユーザーを認証するために呼び出されます。 アプリから要求されるスキームと TestScheme
スキームを一致させることが重要です。 そうでない場合は、認証が機能しません。
[Fact]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddAuthentication(defaultScheme: "TestScheme")
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
"TestScheme", options => { });
});
})
.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false,
});
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue(scheme: "TestScheme");
//Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
WebApplicationFactoryClientOptions
の詳細については、「クライアントのオプション」セクションを参照してください。
認証ミドルウェアの基本的なテスト
認証ミドルウェアの基本的なテストについては、こちらの GitHub リポジトリを参照してください。 これには、テスト シナリオに固有のテスト サーバーが含まれています。
環境を設定する
カスタム アプリケーション ファクトリで環境を設定します。
public class CustomWebApplicationFactory<TProgram>
: WebApplicationFactory<TProgram> where TProgram : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
var dbContextDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbContextOptions<ApplicationDbContext>));
services.Remove(dbContextDescriptor);
var dbConnectionDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbConnection));
services.Remove(dbConnectionDescriptor);
// Create open SqliteConnection so EF won't automatically close it.
services.AddSingleton<DbConnection>(container =>
{
var connection = new SqliteConnection("DataSource=:memory:");
connection.Open();
return connection;
});
services.AddDbContext<ApplicationDbContext>((container, options) =>
{
var connection = container.GetRequiredService<DbConnection>();
options.UseSqlite(connection);
});
});
builder.UseEnvironment("Development");
}
}
テスト インフラストラクチャがアプリ コンテンツのルート パスを推測する方法
WebApplicationFactory
コンストラクターを使用すると、TEntryPoint
アセンブリの System.Reflection.Assembly.FullName
と同じキーを持つ統合テストを含むアセンブリで WebApplicationFactoryContentRootAttribute を検索することによって、アプリのコンテンツ ルート パスを推測できます。 正しいキーを持つ属性が見つからない場合、WebApplicationFactory
はフォールバックしてソリューション ファイル ( .sln) を検索し、TEntryPoint
アセンブリ名をソリューション ディレクトリに追加します。 アプリのルート ディレクトリ (コンテンツ ルート パス) は、ビューやコンテンツのファイルを検出するために使用されます。
シャドウ コピーの無効化
シャドウ コピーを行うと、テストが出力ディレクトリとは異なるディレクトリで実行されます。 テストが Assembly.Location
に関連するファイルの読み込みに依存していて、問題が発生した場合、シャドウ コピーを無効にする必要がある場合があります。
xUnit を使用しているときにシャドウ コピーを無効にするには、正しい構成設定を使用して、テスト プロジェクト ディレクトリに xunit.runner.json
ファイルを作成します。
{
"shadowCopy": false
}
オブジェクトの破棄
IClassFixture
実装のテストを実行した後、xUnit によって を破棄すると、TestServer と WebApplicationFactory
HttpClient も破棄されます。 開発者がインスタンス化したオブジェクトを破棄する必要がある場合は、IClassFixture
の実装で破棄します。 詳細については、「Dispose メソッドの実装」を参照してください。
統合テストのサンプル
サンプル アプリは、次の 2 つのアプリで構成されています。
アプリ | プロジェクト ディレクトリ | 説明 |
---|---|---|
メッセージ アプリ (SUT) | src/RazorPagesProject |
ユーザーは、メッセージの追加、1 つ削除、すべて削除、および分析を行うことができます。 |
アプリをテストする | tests/RazorPagesProject.Tests |
SUT の統合テストに使用されます。 |
テストは、Visual Studio などの IDE に組み込まれているテスト機能を使用して実行できます。 Visual Studio Code またはコマンド ラインを使用している場合は、コマンド プロンプトで tests/RazorPagesProject.Tests
ディレクトリを開き、次のコマンドを実行します。
dotnet test
メッセージ アプリ (SUT) の構成
SUT は、次の特性を持つ Razor Pages メッセージ システムです。
- アプリのインデックス ページ (
Pages/Index.cshtml
とPages/Index.cshtml.cs
) には、メッセージの追加、削除、および分析 (メッセージあたりの平均単語数) を制御する UI およびページ モデル メソッドが用意されています。 - メッセージは、
Id
(キー) とText
(メッセージ) の 2 つのプロパティを持つMessage
クラス (Data/Message.cs
) によって記述されます。Text
プロパティは必須であり、200 文字までに制限されています。 - メッセージは、Entity Framework のメモリ内データベース† を使用して格納されます。
- アプリのデータベース コンテキスト クラスである
AppDbContext
(Data/AppDbContext.cs
) には、データアクセス層 (DAL) が含まれています。 - アプリの起動時にデータベースが空の場合、メッセージ ストアが 3 つのメッセージで初期化されます。
- アプリには、認証されたユーザーのみがアクセスできる
/SecurePage
が含まれています。
†InMemory を使用したテストに関する EF 記事では、MSTest を使用したテストでメモリ内データベースを使用する方法について説明します。 このトピックでは、xUnit テスト フレームワークを使用します。 テストの概念とテストの実装は、異なるテスト フレームワークでも類似していますが、同一ではありません。
アプリはリポジトリ パターンを使用しておらず、Unit of Work (UoW) パターンの有効な例ではありませんが、Razor Pages ではこれらの開発パターンがサポートされています。 詳細については、インフラストラクチャの永続化レイヤーの設計およびコントローラー ロジックのテストに関する記事を参照してください (このサンプルはリポジトリ パターンを実装しています)。
テスト アプリの構成
テスト アプリは、tests/RazorPagesProject.Tests
ディレクトリにあるコンソール アプリです。
テスト アプリのディレクトリ | 説明 |
---|---|
AuthTests |
次のテスト メソッドを含みます。
|
BasicTests |
ルーティングおよびコンテンツ タイプのテスト メソッドを含みます。 |
IntegrationTests |
カスタム WebApplicationFactory クラスを使用したインデックス ページの統合テストを含みます。 |
Helpers/Utilities |
|
テスト フレームワークは、xUnit です。 統合テストは、TestServer を含む Microsoft.AspNetCore.TestHost を使用して実行されます。 Microsoft.AspNetCore.Mvc.Testing
パッケージはテスト ホストとテスト サーバーを構成するために使用されるため、テスト アプリのプロジェクト ファイルまたはテスト アプリの開発者構成で直接 TestHost
および TestServer
パッケージを参照する必要はありません。
統合テストでは、通常、テストを実行する前に、データベース内に小さなデータセットが必要です。 たとえば、削除テストでは、データベース レコードの削除を呼び出します。そのため、削除要求を成功させるには、データベースに少なくとも 1 つのレコードが必要です。
このサンプル アプリでは、Utilities.cs
で 3 つのメッセージを使用してデータベースをシードします。このメッセージは、テストを実行する際に使用することができます。
public static void InitializeDbForTests(ApplicationDbContext db)
{
db.Messages.AddRange(GetSeedingMessages());
db.SaveChanges();
}
public static void ReinitializeDbForTests(ApplicationDbContext db)
{
db.Messages.RemoveRange(db.Messages);
InitializeDbForTests(db);
}
public static List<Message> GetSeedingMessages()
{
return new List<Message>()
{
new Message(){ Text = "TEST RECORD: You're standing on my scarf." },
new Message(){ Text = "TEST RECORD: Would you like a jelly baby?" },
new Message(){ Text = "TEST RECORD: To the rational mind, " +
"nothing is inexplicable; only unexplained." }
};
}
SUT のデータベース コンテキストは、Program.cs
に登録されます。 テスト アプリの builder.ConfigureServices
コールバックは、アプリの Program.cs
コードが実行された後に実行されます。 テストに異なるデータベースを使用するには、アプリのデータベース コンテキストを builder.ConfigureServices
で置き換える必要があります。 詳細については、「WebApplicationFactory のカスタマイズ」セクションをご覧ください。
その他の技術情報
このトピックを読むには、単体テストの基本的な知識があることが前提となります。 テストの概念を理解していない場合は、「.NET Core の単体テストと .NET Standard」 のトピックとリンクされたコンテンツを参照してください。
サンプル コードを表示またはダウンロードします (ダウンロード方法)。
このサンプル アプリは Razor Pages アプリであり、Razor Pages の基本を理解していることを前提としています。 Razor Pages に慣れていない場合は、次のトピックを参照してください。
Note
SPA をテストする場合は、ブラウザーを自動化できる Playwright for .NET などのツールを推奨します。
統合テストの概要
統合テストでは 単体テストよりも広範なレベルでアプリのコンポーネントを評価します。 単体テストは、個々のクラス メソッドなど、分離されたソフトウェア コンポーネントをテストするために使用されます。 統合テストでは、2 つ以上のアプリ コンポーネントが、連携して予想される結果を生成することを確認します。これは、要求を完全に処理するために必要なすべてのコンポーネントを含む可能性があります。
これらの広範なテストは、アプリのインフラストラクチャとフレームワーク全体をテストするために使用されます。多くの場合、次のコンポーネントが含まれます。
- データベース
- ファイル システム
- ネットワーク アプライアンス
- 要求 - 応答パイプライン
単体テストでは、インフラストラクチャ コンポーネントの代わりに、フェイクまたはモック オブジェクトと呼ばれる、作成済みのコンポーネントを使用します。
単体テストと比較すると、統合テストは次のようになります。
- アプリが運用環境で使用する実際のコンポーネントを使用します。
- より多くのコードとデータ処理が必要です。
- 実行に時間がかかります。
そのため、統合テストの使用は最も重要なインフラストラクチャ シナリオに限定します。 単体テストと統合テストのどちらを使用しても動作をテストできる場合は、単体テストを選択します。
統合テストの説明では、テスト対象のプロジェクトのことをよく "テスト対象システム"、または短縮して "SUT" と呼びます。 この記事全体を通して、テスト対象の ASP.NET Core アプリを指すために "SUT" を使用します。
データベースとファイル システムを使用するデータおよびファイル アクセスの "すべての順列として統合テストを記述してはいけません"。 通常は、アプリ全体でデータベースやファイル システムを操作する場所がいくつあったとしても、的を絞った一連の読み取り、書き込み、更新、削除の統合テストを行うことで、データベースおよびファイル システム コンポーネントを適切にテストすることができます。 これらのコンポーネントと連携するメソッドのロジックのルーチン テストには、単体テストを使用します。 単体テストでは、インフラストラクチャのフェイクまたはモックを使用することにより、テストの実行時間が短縮されます。
ASP.NET Core 統合テスト
ASP.NET Core の統合テストには、次のものが必要です。
- テスト プロジェクトは、テストを格納して実行するために使用します。 テスト プロジェクトは SUT への参照を含みます。
- テスト プロジェクトは、SUT のテスト Web ホストを作成し、テスト サーバー クライアントを使用して SUT との要求と応答を処理します。
- テスト ランナーは、テストを実行し、テスト結果を報告するために使用されます。
統合テストでは、通常の Arrange (配置) 、Act (実行) 、および Assert (確認) のテスト ステップを含む一連のイベントに従います。
- SUT の Web ホストが構成されます。
- アプリに要求を送信するためのテスト サーバー クライアントが作成されます。
- Arrange (配置) テスト ステップが実行されます。テスト アプリが要求を準備します。
- Act (実行) テスト ステップが実行されます。クライアントは要求を送信し、応答を受信します。
- Assert (確認) テスト ステップが実行されます。実際の応答は、予測される応答に基づき、成功または失敗として検証されます。
- このプロセスは、すべてのテストが実行されるまで続行されます。
- テスト結果が報告されます。
通常、テスト Web ホストは、アプリの通常のテスト用の Web ホストとは異なる方法で構成されています。 たとえば、テスト用に別のデータベースまたは異なるアプリ設定を使用する場合があります。
テスト Web ホストやメモリ内テスト サーバー (TestServer) などのインフラストラクチャ コンポーネントは、Microsoft.AspNetCore.Mvc.Testing パッケージによって提供または管理されます。 このパッケージを使用すると、テストの作成と実行を効率化できます。
Microsoft.AspNetCore.Mvc.Testing
パッケージは、次のタスクを処理します。
- 依存関係ファイル (
.deps
) を SUT からテスト プロジェクトのbin
ディレクトリにコピーします。 - テストを実行したときに、静的なファイルとページ/ビューが検出されるように、コンテンツ ルートを SUT のプロジェクト ルートに設定します。
- WebApplicationFactory クラスを提供し、
TestServer
を使用して SUT のブートストラップを効率化します。
単体テストのドキュメントでは、テスト プロジェクトとテスト ランナーを設定する方法、テストを実行する方法の詳細な手順、テストおよびテスト クラスの命名方法に関する推奨事項について説明します。
単体テストを統合テストから分離し、異なるプロジェクトにします。 テストの分離は、次の面で役立ちます。
- インフラストラクチャ テスト コンポーネントが誤って単体テストに含まれないようにすることができる。
- 実行されるテストのセットを制御することができる。
Razor Pages アプリと MVC アプリのテストの構成には、ほぼ違いがありません。 唯一の違いは、テストの命名方法です。 Razor Pages アプリでは、ページ エンドポイントのテストは通常、ページ モデル クラスにちなんだ名前が付けられます (たとえば、IndexPageTests
では Index ページのコンポーネントの統合テストが行われます)。 MVC アプリでは、テストは通常、コントローラー クラス別に編成され、テストするコントローラーにちなんだ名前が付けられます (たとえば、HomeControllerTests
は Home コントローラーのコンポーネントの統合テストを行います)。
テスト アプリの前提条件
テスト プロジェクトは、次の条件を満たす必要があります。
Microsoft.AspNetCore.Mvc.Testing
パッケージを参照します。- プロジェクト ファイル (
<Project Sdk="Microsoft.NET.Sdk.Web">
) で Web SDK を指定しています。
これらの前提条件は、サンプル アプリで確認できます。 tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj
ファイルを確認します。 このサンプル アプリでは、xUnit テスト フレームワークと AngleSharp パーサー ライブラリを使用するので、サンプル アプリは以下も参照します。
xunit.runner.visualstudio
バージョン 2.4.2 以降を使用するアプリでは、テスト プロジェクトによって Microsoft.NET.Test.Sdk
パッケージが参照される必要があります。
テストでは Entity Framework Core も使用します。 アプリは以下を参照します。
Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
Microsoft.AspNetCore.Identity.EntityFrameworkCore
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.InMemory
Microsoft.EntityFrameworkCore.Tools
SUT 環境
SUT の 環境 が設定されていない場合、環境は既定で開発になります。
既定の WebApplicationFactory を使用した基本的なテスト
WebApplicationFactory<TEntryPoint> は、統合テスト用の TestServer を作成するために使用します。 TEntryPoint
は SUT のエントリ ポイント クラスであり、通常は Startup
クラスです。
テスト クラスでは、クラスにテストが含まれていることを示すために "クラス フィクスチャ" インターフェイス (IClassFixture
) を実装し、クラス内のテストの共有オブジェクト インスタンスを提供します。
次のテスト クラス BasicTests
では、WebApplicationFactory
を使用して SUT をブートストラップし、テスト メソッド Get_EndpointsReturnSuccessAndCorrectContentType
に HttpClient を提供します。 このメソッドは、複数のアプリ ページで応答状態コードが成功かどうか (200-299 の範囲の状態コード) と、Content-Type
ヘッダーが text/html; charset=utf-8
であるかどうかを確認します。
CreateClient() では、自動的にリダイレクトに従い、Cookie を処理する、HttpClient
のインスタンスを作成します。
public class BasicTests
: IClassFixture<WebApplicationFactory<RazorPagesProject.Startup>>
{
private readonly WebApplicationFactory<RazorPagesProject.Startup> _factory;
public BasicTests(WebApplicationFactory<RazorPagesProject.Startup> factory)
{
_factory = factory;
}
[Theory]
[InlineData("/")]
[InlineData("/Index")]
[InlineData("/About")]
[InlineData("/Privacy")]
[InlineData("/Contact")]
public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
{
// Arrange
var client = _factory.CreateClient();
// Act
var response = await client.GetAsync(url);
// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.Equal("text/html; charset=utf-8",
response.Content.Headers.ContentType.ToString());
}
}
既定では、GDPR 同意ポリシーが有効になっている場合、要求間で必須でない Cookie は保持されません。 TempData プロバイダーで使用されているような必須ではない Cookie を保持するには、テストに必須であることをマークします。 cookie を必須としてマークする手順については、必須 Cookie をご覧ください。
WebApplicationFactory のカスタマイズ
Web ホストの構成は、WebApplicationFactory
から継承して 1 つ以上のカスタム ファクトリを作成することで、テスト クラスとは別に作成できます。
WebApplicationFactory
から継承し、ConfigureWebHost をオーバーライドします。 IWebHostBuilder では、ConfigureServices を使用してサービス コレクションを構成できます。public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup: class { protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureServices(services => { var descriptor = services.SingleOrDefault( d => d.ServiceType == typeof(DbContextOptions<ApplicationDbContext>)); services.Remove(descriptor); services.AddDbContext<ApplicationDbContext>(options => { options.UseInMemoryDatabase("InMemoryDbForTesting"); }); var sp = services.BuildServiceProvider(); using (var scope = sp.CreateScope()) { var scopedServices = scope.ServiceProvider; var db = scopedServices.GetRequiredService<ApplicationDbContext>(); var logger = scopedServices .GetRequiredService<ILogger<CustomWebApplicationFactory<TStartup>>>(); db.Database.EnsureCreated(); try { Utilities.InitializeDbForTests(db); } catch (Exception ex) { logger.LogError(ex, "An error occurred seeding the " + "database with test messages. Error: {Message}", ex.Message); } } }); } }
サンプル アプリでのデータベースのシード処理は、
InitializeDbForTests
メソッドによって実行されます。 このメソッドについては、統合テストのサンプル: テスト アプリの構成に関するセクションをご覧ください。SUT のデータベース コンテキストは、
Startup.ConfigureServices
メソッドに登録されます。 テスト アプリのbuilder.ConfigureServices
コールバックは、アプリのStartup.ConfigureServices
コードが実行された後に実行されます。 ASP.NET Core 3.0 リリースの汎用ホストにより、実行順序に関する互換性のない変更が行われています。 アプリのデータベースとは異なるデータベースをテストに使用するには、builder.ConfigureServices
でアプリのデータベース コンテキストを置き換える必要があります。まだ Web ホストを使用している SUT の場合、テスト アプリの
builder.ConfigureServices
コールバックは、SUT のStartup.ConfigureServices
コードの "前に" 実行されます。 テスト アプリのbuilder.ConfigureTestServices
コールバックは、"後で" 実行されます。サンプル アプリでは、データベース コンテキストのサービス記述子を検索し、記述子を使用してサービス登録を削除しています。 次に、ファクトリは、テストにメモリ内データベースを使用する新しい
ApplicationDbContext
を追加します。メモリ内データベースではないデータベースに接続するには、
UseInMemoryDatabase
呼び出しを変更して、コンテキストを別のデータベースに接続します。 SQL Server テスト データベースを使用するには、次のようにします。- プロジェクト ファイルで
Microsoft.EntityFrameworkCore.SqlServer
NuGet パッケージを参照します。 - データベースへの接続文字列を使用して
UseSqlServer
を呼び出します。
services.AddDbContext<ApplicationDbContext>((options, context) => { context.UseSqlServer( Configuration.GetConnectionString("TestingDbConnectionString")); });
- プロジェクト ファイルで
テスト クラスでカスタム
CustomWebApplicationFactory
を使用します。 次の例では、IndexPageTests
クラスでファクトリを使用しています。public class IndexPageTests : IClassFixture<CustomWebApplicationFactory<RazorPagesProject.Startup>> { private readonly HttpClient _client; private readonly CustomWebApplicationFactory<RazorPagesProject.Startup> _factory; public IndexPageTests( CustomWebApplicationFactory<RazorPagesProject.Startup> factory) { _factory = factory; _client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); }
サンプル アプリのクライアントは、
HttpClient
がリダイレクトに従わないように構成されています。 モック認証のセクションで後述するように、これによってアプリの最初の応答結果を確認するテストが可能になります。 これらのテストでは、多くの場合、最初の応答はLocation
ヘッダーを持つリダイレクトです。一般的なテストでは、
HttpClient
およびヘルパー メソッドを使用して、要求と応答を処理します。[Fact] public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot() { // Arrange var defaultPage = await _client.GetAsync("/"); var content = await HtmlHelpers.GetDocumentAsync(defaultPage); //Act var response = await _client.SendAsync( (IHtmlFormElement)content.QuerySelector("form[id='messages']"), (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']")); // Assert Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode); Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); Assert.Equal("/", response.Headers.Location.OriginalString); }
SUT に対する POST 要求は、アプリの偽造防止データ保護システムによって自動的に行われる偽造防止チェックを満たす必要があります。 テストで POST 要求を実行するには、テスト アプリで次のことを行う必要があります。
- ページに対して要求を行います。
- 応答の偽造防止 cookie と要求検証トークンを解析します。
- 偽造防止 cookie と要求検証トークンを使用して POST 要求を行います。
サンプル アプリの SendAsync
ヘルパー拡張メソッド (Helpers/HttpClientExtensions.cs
) と GetDocumentAsync
ヘルパー メソッド (Helpers/HtmlHelpers.cs
) は、次のメソッドで AngleSharp パーサーを使用して偽造防止チェック処理を行います。
GetDocumentAsync
: HttpResponseMessage を受け取り、IHtmlDocument
を返します。GetDocumentAsync
は、元のHttpResponseMessage
に基づいて仮想応答を準備するファクトリを使用します。 詳しくは、AngleSharp のドキュメントをご覧ください。HttpClient
のSendAsync
拡張メソッドを使って HttpRequestMessage を作成し、SendAsync(HttpRequestMessage) を呼び出して SUT に要求を送信します。SendAsync
のオーバーロードは、HTML フォーム (IHtmlFormElement
) と次のものを受け入れます。- フォームの送信ボタン (
IHtmlElement
) - フォームの値コレクション (
IEnumerable<KeyValuePair<string, string>>
) - 送信ボタン (
IHtmlElement
) とフォームの値 (IEnumerable<KeyValuePair<string, string>>
)
- フォームの送信ボタン (
Note
AngleSharp は、このトピックとサンプル アプリのデモンストレーションのために使用するサードパーティ製の解析ライブラリです。 ASP.NET Core アプリの統合テストでは、AngleSharp はサポートされていないか、必要ありません。 Html Agility Pack (HAP) などの他のパーサーを使用することもできます。 もう 1 つの方法として、偽造防止システムの要求検証トークンを処理するコードを記述し、偽造防止 cookie を直接処理する方法もあります。
Note
EF-Core インメモリ データベース プロバイダーは、限定された基本的なテストに使用できます。一方、SQLite プロバイダーは、インメモリ テストに推奨される選択肢です。
WithWebHostBuilder を使用したクライアントのカスタマイズ
テスト メソッド内で追加の構成が必要な場合、WithWebHostBuilder では、IWebHostBuilder を使用して新しい WebApplicationFactory
を作成できます。
サンプル アプリ の Post_DeleteMessageHandler_ReturnsRedirectToRoot
テスト メソッドは、WithWebHostBuilder
の使用方法を示しています。 このテストでは、SUT からフォーム送信をトリガーすることによって、データベース内のレコードの削除を実行します。
IndexPageTests
クラス内の別のテストは、データベース内のすべてのレコードを削除する操作を実行します。この操作が Post_DeleteMessageHandler_ReturnsRedirectToRoot
メソッドの前に実行される可能性があるため、このテスト メソッド内でデータベースの再シード処理を行い、確実に SUT が削除するレコードが存在するようにしています。 SUT に対する要求では、SUT の messages
フォームの最初の [削除] ボタンの選択がシミュレートされます。
[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
var serviceProvider = services.BuildServiceProvider();
using (var scope = serviceProvider.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var db = scopedServices
.GetRequiredService<ApplicationDbContext>();
var logger = scopedServices
.GetRequiredService<ILogger<IndexPageTests>>();
try
{
Utilities.ReinitializeDbForTests(db);
}
catch (Exception ex)
{
logger.LogError(ex, "An error occurred seeding " +
"the database with test messages. Error: {Message}",
ex.Message);
}
}
});
})
.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
var defaultPage = await client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
//Act
var response = await client.SendAsync(
(IHtmlFormElement)content.QuerySelector("form[id='messages']"),
(IHtmlButtonElement)content.QuerySelector("form[id='messages']")
.QuerySelector("div[class='panel-body']")
.QuerySelector("button"));
// Assert
Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("/", response.Headers.Location.OriginalString);
}
クライアントのオプション
次の表に、HttpClient
インスタンスを作成するときに使用できる既定の WebApplicationFactoryClientOptions を示します。
オプション | 説明 | Default |
---|---|---|
AllowAutoRedirect | HttpClient インスタンスがリダイレクト応答に自動的に従うかどうかを取得または設定します。 |
true |
BaseAddress | HttpClient インスタンスのベース アドレスを取得または設定します。 |
http://localhost |
HandleCookies | HttpClient インスタンスが Cookie を処理する必要があるかどうかを取得または設定します。 |
true |
MaxAutomaticRedirections | HttpClient インスタンスが従う必要があるリダイレクト応答の最大数を取得または設定します。 |
7 |
WebApplicationFactoryClientOptions
クラスを作成し、それを CreateClient() メソッドに渡します (既定値については、コード例を参照してください)。
// Default client option values are shown
var clientOptions = new WebApplicationFactoryClientOptions();
clientOptions.AllowAutoRedirect = true;
clientOptions.BaseAddress = new Uri("http://localhost");
clientOptions.HandleCookies = true;
clientOptions.MaxAutomaticRedirections = 7;
_client = _factory.CreateClient(clientOptions);
モック サービスの注入
ホスト ビルダーで ConfigureTestServices を呼び出すと、テストでサービスをオーバーライドできます。 モック サービスを注入するには、SUT に Startup
クラスが存在し、そこに Startup.ConfigureServices
メソッドが存在している必要があります。
サンプルの SUT には、引用符を返すスコープ サービスが含まれています。 インデックス ページが要求されると、インデックス ページの非表示フィールドに引用符が埋め込まれます。
Services/IQuoteService.cs
:
public interface IQuoteService
{
Task<string> GenerateQuote();
}
Services/QuoteService.cs
:
// Quote ©1975 BBC: The Doctor (Tom Baker); Dr. Who: Planet of Evil
// https://www.bbc.co.uk/programmes/p00pyrx6
public class QuoteService : IQuoteService
{
public Task<string> GenerateQuote()
{
return Task.FromResult<string>(
"Come on, Sarah. We've an appointment in London, " +
"and we're already 30,000 years late.");
}
}
Startup.cs
:
services.AddScoped<IQuoteService, QuoteService>();
Pages/Index.cshtml.cs
:
public class IndexModel : PageModel
{
private readonly ApplicationDbContext _db;
private readonly IQuoteService _quoteService;
public IndexModel(ApplicationDbContext db, IQuoteService quoteService)
{
_db = db;
_quoteService = quoteService;
}
[BindProperty]
public Message Message { get; set; }
public IList<Message> Messages { get; private set; }
[TempData]
public string MessageAnalysisResult { get; set; }
public string Quote { get; private set; }
public async Task OnGetAsync()
{
Messages = await _db.GetMessagesAsync();
Quote = await _quoteService.GenerateQuote();
}
Pages/Index.cs
:
<input id="quote" type="hidden" value="@Model.Quote">
次のマークアップは、SUT アプリの実行時に生成されます。
<input id="quote" type="hidden" value="Come on, Sarah. We've an appointment in
London, and we're already 30,000 years late.">
統合テストでサービスと引用符の注入をテストするため、テストは SUT にモック サービスを注入します。 モック サービスは、アプリの QuoteService
をテスト アプリが提供する TestQuoteService
と呼ばれるサービスに置き換えます。
IntegrationTests.IndexPageTests.cs
:
// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
public Task<string> GenerateQuote()
{
return Task.FromResult<string>(
"Something's interfering with time, Mr. Scarman, " +
"and time is my business.");
}
}
ConfigureTestServices
が呼び出され、スコープ サービスが登録されます。
[Fact]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddScoped<IQuoteService, TestQuoteService>();
});
})
.CreateClient();
//Act
var defaultPage = await client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
var quoteElement = content.QuerySelector("#quote");
// Assert
Assert.Equal("Something's interfering with time, Mr. Scarman, " +
"and time is my business.", quoteElement.Attributes["value"].Value);
}
TestQuoteService
によって指定された引用符テキストがテストの実行中に生成されたマークアップに反映されるため、アサーションは成功します。
<input id="quote" type="hidden" value="Something's interfering with time,
Mr. Scarman, and time is my business.">
モック認証
AuthTests
クラスのテストは、セキュリティで保護されたエンドポイントであることを確認します。
- 認証されていないユーザーは、アプリのログイン ページにリダイレクトされます。
- 認証されたユーザーには、コンテンツを返します。
SUT の /SecurePage
ページでは、AuthorizePage 規約を使用してページに AuthorizeFilter を適用します。 詳細については、Razor Pages の承認規則に関するページを参照してください。
services.AddRazorPages(options =>
{
options.Conventions.AuthorizePage("/SecurePage");
});
Get_SecurePageRedirectsAnUnauthenticatedUser
テストでは、AllowAutoRedirect を false
に設定することで、WebApplicationFactoryClientOptions がリダイレクトを許可しないように設定しています。
[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
// Arrange
var client = _factory.CreateClient(
new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
// Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.StartsWith("http://localhost/Identity/Account/Login",
response.Headers.Location.OriginalString);
}
クライアントがリダイレクトに従うことを許可しないことで、次のチェックを行うことができます。
- SUT によって返される状態コードを確認するには、ログイン ページにリダイレクトした後の最終的な状態コード (これは HttpStatusCode.OK になります) ではなく、予期される HttpStatusCode.Redirect の結果と照らし合わせます。
- 最終的なログイン ページ応答 (ここでは、
Location
ヘッダーは存在しません) ではなく、応答ヘッダーのLocation
ヘッダー値がhttp://localhost/Identity/Account/Login
で始まることを確認できます。
テスト アプリでは、認証と承認の側面をテストするために ConfigureTestServices の AuthenticationHandler<TOptions> をモックすることができます。 最小のシナリオでは、AuthenticateResult.Success が返されます。
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "Test");
var result = AuthenticateResult.Success(ticket);
return Task.FromResult(result);
}
}
TestAuthHandler
は、認証スキームが Test
に設定されている場合 (この場合、AddAuthentication
が ConfigureTestServices
に登録されています) に、ユーザーを認証するために呼び出されます。 アプリから要求されるスキームと Test
スキームを一致させることが重要です。 そうでない場合は、認証が機能しません。
[Fact]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddAuthentication("Test")
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
"Test", options => {});
});
})
.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false,
});
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Test");
//Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
WebApplicationFactoryClientOptions
の詳細については、「クライアントのオプション」セクションを参照してください。
環境を設定する
既定では、SUT のホストとアプリ環境は、開発環境を使用するように構成されています。 IHostBuilder
を使用しているときに SUT の環境をオーバーライドするには、次のようにします。
ASPNETCORE_ENVIRONMENT
環境変数 (たとえば、Staging
、Production
、またはTesting
などのカスタム値) を設定します。- テスト アプリで
CreateHostBuilder
をオーバーライドして、ASPNETCORE
で始まる環境変数を読み取ります。
protected override IHostBuilder CreateHostBuilder() =>
base.CreateHostBuilder()
.ConfigureHostConfiguration(
config => config.AddEnvironmentVariables("ASPNETCORE"));
SUT が Web ホスト (IWebHostBuilder
) を使用している場合は、CreateWebHostBuilder
をオーバーライドします。
protected override IWebHostBuilder CreateWebHostBuilder() =>
base.CreateWebHostBuilder().UseEnvironment("Testing");
テスト インフラストラクチャがアプリ コンテンツのルート パスを推測する方法
WebApplicationFactory
コンストラクターを使用すると、TEntryPoint
アセンブリの System.Reflection.Assembly.FullName
と同じキーを持つ統合テストを含むアセンブリで WebApplicationFactoryContentRootAttribute を検索することによって、アプリのコンテンツ ルート パスを推測できます。 正しいキーを持つ属性が見つからない場合、WebApplicationFactory
はフォールバックしてソリューション ファイル ( .sln) を検索し、TEntryPoint
アセンブリ名をソリューション ディレクトリに追加します。 アプリのルート ディレクトリ (コンテンツ ルート パス) は、ビューやコンテンツのファイルを検出するために使用されます。
シャドウ コピーの無効化
シャドウ コピーを行うと、テストが出力ディレクトリとは異なるディレクトリで実行されます。 テストが Assembly.Location
に関連するファイルの読み込みに依存していて、問題が発生した場合、シャドウ コピーを無効にする必要がある場合があります。
xUnit を使用しているときにシャドウ コピーを無効にするには、正しい構成設定を使用して、テスト プロジェクト ディレクトリに xunit.runner.json
ファイルを作成します。
{
"shadowCopy": false
}
オブジェクトの破棄
IClassFixture
実装のテストを実行した後、xUnit によって を破棄すると、TestServer と WebApplicationFactory
HttpClient も破棄されます。 開発者がインスタンス化したオブジェクトを破棄する必要がある場合は、IClassFixture
の実装で破棄します。 詳細については、「Dispose メソッドの実装」を参照してください。
統合テストのサンプル
サンプル アプリは、次の 2 つのアプリで構成されています。
アプリ | プロジェクト ディレクトリ | 説明 |
---|---|---|
メッセージ アプリ (SUT) | src/RazorPagesProject |
ユーザーは、メッセージの追加、1 つ削除、すべて削除、および分析を行うことができます。 |
アプリをテストする | tests/RazorPagesProject.Tests |
SUT の統合テストに使用されます。 |
テストは、Visual Studio などの IDE に組み込まれているテスト機能を使用して実行できます。 Visual Studio Code またはコマンド ラインを使用している場合は、コマンド プロンプトで tests/RazorPagesProject.Tests
ディレクトリを開き、次のコマンドを実行します。
dotnet test
メッセージ アプリ (SUT) の構成
SUT は、次の特性を持つ Razor Pages メッセージ システムです。
- アプリのインデックス ページ (
Pages/Index.cshtml
とPages/Index.cshtml.cs
) には、メッセージの追加、削除、および分析 (メッセージあたりの平均単語数) を制御する UI およびページ モデル メソッドが用意されています。 - メッセージは、
Id
(キー) とText
(メッセージ) の 2 つのプロパティを持つMessage
クラス (Data/Message.cs
) によって記述されます。Text
プロパティは必須であり、200 文字までに制限されています。 - メッセージは、Entity Framework のメモリ内データベース† を使用して格納されます。
- アプリのデータベース コンテキスト クラスである
AppDbContext
(Data/AppDbContext.cs
) には、データアクセス層 (DAL) が含まれています。 - アプリの起動時にデータベースが空の場合、メッセージ ストアが 3 つのメッセージで初期化されます。
- アプリには、認証されたユーザーのみがアクセスできる
/SecurePage
が含まれています。
†InMemory を使用したテストに関する EF トピックでは、MSTest を使用したテストでメモリ内データベースを使用する方法について説明しています。 このトピックでは、xUnit テスト フレームワークを使用します。 テストの概念とテストの実装は、異なるテスト フレームワークでも類似していますが、同一ではありません。
アプリはリポジトリ パターンを使用しておらず、Unit of Work (UoW) パターンの有効な例ではありませんが、Razor Pages ではこれらの開発パターンがサポートされています。 詳細については、インフラストラクチャの永続化レイヤーの設計およびコントローラー ロジックのテストに関する記事を参照してください (このサンプルはリポジトリ パターンを実装しています)。
テスト アプリの構成
テスト アプリは、tests/RazorPagesProject.Tests
ディレクトリにあるコンソール アプリです。
テスト アプリのディレクトリ | 説明 |
---|---|
AuthTests |
次のテスト メソッドを含みます。
|
BasicTests |
ルーティングおよびコンテンツ タイプのテスト メソッドを含みます。 |
IntegrationTests |
カスタム WebApplicationFactory クラスを使用したインデックス ページの統合テストを含みます。 |
Helpers/Utilities |
|
テスト フレームワークは、xUnit です。 統合テストは、TestServer を含む Microsoft.AspNetCore.TestHost を使用して実行されます。 Microsoft.AspNetCore.Mvc.Testing
パッケージはテスト ホストとテスト サーバーを構成するために使用されるため、テスト アプリのプロジェクト ファイルまたはテスト アプリの開発者構成で直接 TestHost
および TestServer
パッケージを参照する必要はありません。
統合テストでは、通常、テストを実行する前に、データベース内に小さなデータセットが必要です。 たとえば、削除テストでは、データベース レコードの削除を呼び出します。そのため、削除要求を成功させるには、データベースに少なくとも 1 つのレコードが必要です。
このサンプル アプリでは、Utilities.cs
で 3 つのメッセージを使用してデータベースをシードします。このメッセージは、テストを実行する際に使用することができます。
public static void InitializeDbForTests(ApplicationDbContext db)
{
db.Messages.AddRange(GetSeedingMessages());
db.SaveChanges();
}
public static void ReinitializeDbForTests(ApplicationDbContext db)
{
db.Messages.RemoveRange(db.Messages);
InitializeDbForTests(db);
}
public static List<Message> GetSeedingMessages()
{
return new List<Message>()
{
new Message(){ Text = "TEST RECORD: You're standing on my scarf." },
new Message(){ Text = "TEST RECORD: Would you like a jelly baby?" },
new Message(){ Text = "TEST RECORD: To the rational mind, " +
"nothing is inexplicable; only unexplained." }
};
}
SUT のデータベース コンテキストは、Startup.ConfigureServices
メソッドに登録されます。 テスト アプリの builder.ConfigureServices
コールバックは、アプリの Startup.ConfigureServices
コードが実行された後に実行されます。 テストに異なるデータベースを使用するには、アプリのデータベース コンテキストを builder.ConfigureServices
で置き換える必要があります。 詳細については、「WebApplicationFactory のカスタマイズ」セクションをご覧ください。
まだ Web ホストを使用している SUT の場合、テスト アプリの builder.ConfigureServices
コールバックは、SUT の Startup.ConfigureServices
コードの "前に" 実行されます。 テスト アプリの builder.ConfigureTestServices
コールバックは、"後で" 実行されます。
その他の技術情報
この記事を読むには、単体テストの基本的な知識があることが前提となります。 テストの概念に慣れていない場合は、.NET Core と .NET Standard の単体テストに関する記事とそのリンクされたコンテンツを参照してください。
サンプル コードを表示またはダウンロードします (ダウンロード方法)。
このサンプル アプリは Razor Pages アプリであり、Razor Pages の基本を理解していることを前提としています。 Razor Pages に慣れていない場合は、次の記事を参照してください。
SPA をテストする場合は、ブラウザーを自動化できる Playwright for .NET などのツールをお勧めします。
統合テストの概要
統合テストでは 単体テストよりも広範なレベルでアプリのコンポーネントを評価します。 単体テストは、個々のクラス メソッドなど、分離されたソフトウェア コンポーネントをテストするために使用されます。 統合テストでは、2 つ以上のアプリ コンポーネントが、連携して予想される結果を生成することを確認します。これは、要求を完全に処理するために必要なすべてのコンポーネントを含む可能性があります。
これらの広範なテストは、アプリのインフラストラクチャとフレームワーク全体をテストするために使用されます。多くの場合、次のコンポーネントが含まれます。
- データベース
- ファイル システム
- ネットワーク アプライアンス
- 要求 - 応答パイプライン
単体テストでは、インフラストラクチャ コンポーネントの代わりに、フェイクまたはモック オブジェクトと呼ばれる、作成済みのコンポーネントを使用します。
単体テストと比較すると、統合テストは次のようになります。
- アプリが運用環境で使用する実際のコンポーネントを使用します。
- より多くのコードとデータ処理が必要です。
- 実行に時間がかかります。
そのため、統合テストの使用は最も重要なインフラストラクチャ シナリオに限定します。 単体テストと統合テストのどちらを使用しても動作をテストできる場合は、単体テストを選択します。
統合テストの説明では、テスト対象のプロジェクトのことをよく "テスト対象システム"、または短縮して "SUT" と呼びます。 この記事全体を通して、テスト対象の ASP.NET Core アプリを指すために "SUT" を使用します。
データベースとファイル システムを使用するデータおよびファイル アクセスの "すべての順列として統合テストを記述してはいけません"。 通常は、アプリ全体でデータベースやファイル システムを操作する場所がいくつあったとしても、的を絞った一連の読み取り、書き込み、更新、削除の統合テストを行うことで、データベースおよびファイル システム コンポーネントを適切にテストすることができます。 これらのコンポーネントと連携するメソッドのロジックのルーチン テストには、単体テストを使用します。 単体テストでは、インフラストラクチャのフェイクまたはモックを使用することにより、テストの実行時間が短縮されます。
ASP.NET Core 統合テスト
ASP.NET Core の統合テストには、次のものが必要です。
- テスト プロジェクトは、テストを格納して実行するために使用します。 テスト プロジェクトは SUT への参照を含みます。
- テスト プロジェクトは、SUT のテスト Web ホストを作成し、テスト サーバー クライアントを使用して SUT との要求と応答を処理します。
- テスト ランナーは、テストを実行し、テスト結果を報告するために使用されます。
統合テストでは、通常の Arrange (配置) 、Act (実行) 、および Assert (確認) のテスト ステップを含む一連のイベントに従います。
- SUT の Web ホストが構成されます。
- アプリに要求を送信するためのテスト サーバー クライアントが作成されます。
- Arrange (配置) テスト ステップが実行されます。テスト アプリが要求を準備します。
- Act (実行) テスト ステップが実行されます。クライアントは要求を送信し、応答を受信します。
- Assert (確認) テスト ステップが実行されます。実際の応答は、予測される応答に基づき、成功または失敗として検証されます。
- このプロセスは、すべてのテストが実行されるまで続行されます。
- テスト結果が報告されます。
通常、テスト Web ホストは、アプリの通常のテスト用の Web ホストとは異なる方法で構成されています。 たとえば、テスト用に別のデータベースまたは異なるアプリ設定を使用する場合があります。
テスト Web ホストやメモリ内テスト サーバー (TestServer) などのインフラストラクチャ コンポーネントは、Microsoft.AspNetCore.Mvc.Testing パッケージによって提供または管理されます。 このパッケージを使用すると、テストの作成と実行を効率化できます。
Microsoft.AspNetCore.Mvc.Testing
パッケージは、次のタスクを処理します。
- 依存関係ファイル (
.deps
) を SUT からテスト プロジェクトのbin
ディレクトリにコピーします。 - テストを実行したときに、静的なファイルとページ/ビューが検出されるように、コンテンツ ルートを SUT のプロジェクト ルートに設定します。
- WebApplicationFactory クラスを提供し、
TestServer
を使用して SUT のブートストラップを効率化します。
単体テストのドキュメントでは、テスト プロジェクトとテスト ランナーを設定する方法、テストを実行する方法の詳細な手順、テストおよびテスト クラスの命名方法に関する推奨事項について説明します。
単体テストを統合テストから分離し、異なるプロジェクトにします。 テストの分離は、次の面で役立ちます。
- インフラストラクチャ テスト コンポーネントが誤って単体テストに含まれないようにすることができる。
- 実行されるテストのセットを制御することができる。
Razor Pages アプリと MVC アプリのテストの構成には、ほぼ違いがありません。 唯一の違いは、テストの命名方法です。 Razor Pages アプリでは、ページ エンドポイントのテストは通常、ページ モデル クラスにちなんだ名前が付けられます (たとえば、IndexPageTests
では Index ページのコンポーネントの統合テストが行われます)。 MVC アプリでは、テストは通常、コントローラー クラス別に編成され、テストするコントローラーにちなんだ名前が付けられます (たとえば、HomeControllerTests
は Home コントローラーのコンポーネントの統合テストを行います)。
テスト アプリの前提条件
テスト プロジェクトは、次の条件を満たす必要があります。
Microsoft.AspNetCore.Mvc.Testing
パッケージを参照します。- プロジェクト ファイル (
<Project Sdk="Microsoft.NET.Sdk.Web">
) で Web SDK を指定しています。
これらの前提条件は、サンプル アプリで確認できます。 tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj
ファイルを確認します。 このサンプル アプリでは、xUnit テスト フレームワークと AngleSharp パーサー ライブラリを使用するので、サンプル アプリは以下も参照します。
xunit.runner.visualstudio
バージョン 2.4.2 以降を使用するアプリでは、テスト プロジェクトによって Microsoft.NET.Test.Sdk
パッケージが参照される必要があります。
テストでは Entity Framework Core も使用します。 GitHub のプロジェクト ファイルを参照してください。
SUT 環境
SUT の 環境 が設定されていない場合、環境は既定で開発になります。
既定の WebApplicationFactory を使用した基本的なテスト
次のいずれかの操作を行って、暗黙的に定義された Program
クラスをテスト プロジェクトに公開します。
Web アプリからテスト プロジェクトに内部型を公開します。 これは、SUT プロジェクトのファイル (
.csproj
) 内で実行できます。<ItemGroup> <InternalsVisibleTo Include="MyTestProject" /> </ItemGroup>
部分クラス宣言を使用して、
Program
クラスをパブリックにします。var builder = WebApplication.CreateBuilder(args); // ... Configure services, routes, etc. app.Run(); + public partial class Program { }
サンプル アプリでは、
Program
部分クラスの方法を使用しています。
WebApplicationFactory<TEntryPoint> は、統合テスト用の TestServer を作成するために使用します。 TEntryPoint
は SUT のエントリ ポイント クラスであり、通常は Program.cs
です。
テスト クラスでは、クラスにテストが含まれていることを示すために "クラス フィクスチャ" インターフェイス (IClassFixture
) を実装し、クラス内のテストの共有オブジェクト インスタンスを提供します。
次のテスト クラス BasicTests
では、WebApplicationFactory
を使用して SUT をブートストラップし、テスト メソッド Get_EndpointsReturnSuccessAndCorrectContentType
に HttpClient を提供します。 このメソッドは、複数のアプリ ページで応答状態コードが成功かどうか (200-299) と、Content-Type
ヘッダーが text/html; charset=utf-8
であるかどうかを確認します。
CreateClient() では、自動的にリダイレクトに従い、Cookie を処理する、HttpClient
のインスタンスを作成します。
public class BasicTests
: IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public BasicTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
}
[Theory]
[InlineData("/")]
[InlineData("/Index")]
[InlineData("/About")]
[InlineData("/Privacy")]
[InlineData("/Contact")]
public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
{
// Arrange
var client = _factory.CreateClient();
// Act
var response = await client.GetAsync(url);
// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.Equal("text/html; charset=utf-8",
response.Content.Headers.ContentType.ToString());
}
}
既定では、一般データ保護規則同意ポリシーが有効になっている場合、要求間で必須でない Cookie は保持されません。 TempData プロバイダーで使用されているような必須ではない Cookie を保持するには、テストに必須であることをマークします。 cookie を必須としてマークする手順については、必須 Cookie をご覧ください。
偽造防止チェックのための AngleSharp と Application Parts
この記事では、AngleSharp パーサーを使い、ページを読み込んで HTML を解析することで、偽造防止チェックを処理します。 コントローラーと Razor Pages ビューのエンドポイントを低レベルで、ブラウザーでどのようにレンダリングされるかを気にせずテストする場合は、Application Parts
の使用を検討してください。 Application Parts の方法では、コントローラーまたは Razor Page をアプリに挿入します。これは必要な値を取得する JSON 要求を行うために使用できます。 詳細については、Martin Costello によるブログ「Application Parts を使用した偽造防止によって保護される ASP.NET Core リソースの統合テスト」と関連する GitHub リポジトリを参照してください。
WebApplicationFactory のカスタマイズ
Web ホストの構成は、WebApplicationFactory<TEntryPoint> から継承して 1 つ以上のカスタム ファクトリを作成することで、テスト クラスとは別に作成できます。
WebApplicationFactory
から継承し、ConfigureWebHost をオーバーライドします。 IWebHostBuilder では、IWebHostBuilder.ConfigureServices
を使用してサービス コレクションを構成できます。public class CustomWebApplicationFactory<TProgram> : WebApplicationFactory<TProgram> where TProgram : class { protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureServices(services => { var dbContextDescriptor = services.SingleOrDefault( d => d.ServiceType == typeof(DbContextOptions<ApplicationDbContext>)); services.Remove(dbContextDescriptor); var dbConnectionDescriptor = services.SingleOrDefault( d => d.ServiceType == typeof(DbConnection)); services.Remove(dbConnectionDescriptor); // Create open SqliteConnection so EF won't automatically close it. services.AddSingleton<DbConnection>(container => { var connection = new SqliteConnection("DataSource=:memory:"); connection.Open(); return connection; }); services.AddDbContext<ApplicationDbContext>((container, options) => { var connection = container.GetRequiredService<DbConnection>(); options.UseSqlite(connection); }); }); builder.UseEnvironment("Development"); } }
サンプル アプリでのデータベースのシード処理は、
InitializeDbForTests
メソッドによって実行されます。 このメソッドについては、統合テストのサンプル: テスト アプリの構成に関するセクションをご覧ください。SUT のデータベース コンテキストは、
Program.cs
に登録されます。 テスト アプリのbuilder.ConfigureServices
コールバックは、アプリのProgram.cs
コードが実行された後に実行されます。 アプリのデータベースとは異なるデータベースをテストに使用するには、builder.ConfigureServices
でアプリのデータベース コンテキストを置き換える必要があります。サンプル アプリでは、データベース コンテキストのサービス記述子を検索し、記述子を使用してサービス登録を削除しています。 次に、ファクトリは、テストにメモリ内データベースを使用する新しい
ApplicationDbContext
を追加します。別のデータベースに接続するには、
DbConnection
を変更します。 SQL Server テスト データベースを使用するには、次のようにします。
- プロジェクト ファイルで
Microsoft.EntityFrameworkCore.SqlServer
NuGet パッケージを参照します。 UseInMemoryDatabase
を呼び出します。
テスト クラスでカスタム
CustomWebApplicationFactory
を使用します。 次の例では、IndexPageTests
クラスでファクトリを使用しています。public class IndexPageTests : IClassFixture<CustomWebApplicationFactory<Program>> { private readonly HttpClient _client; private readonly CustomWebApplicationFactory<Program> _factory; public IndexPageTests( CustomWebApplicationFactory<Program> factory) { _factory = factory; _client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); }
サンプル アプリのクライアントは、
HttpClient
がリダイレクトに従わないように構成されています。 モック認証のセクションで後述するように、これによってアプリの最初の応答結果を確認するテストが可能になります。 これらのテストでは、多くの場合、最初の応答はLocation
ヘッダーを持つリダイレクトです。一般的なテストでは、
HttpClient
およびヘルパー メソッドを使用して、要求と応答を処理します。[Fact] public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot() { // Arrange var defaultPage = await _client.GetAsync("/"); var content = await HtmlHelpers.GetDocumentAsync(defaultPage); //Act var response = await _client.SendAsync( (IHtmlFormElement)content.QuerySelector("form[id='messages']"), (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']")); // Assert Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode); Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); Assert.Equal("/", response.Headers.Location.OriginalString); }
SUT に対する POST 要求は、アプリの偽造防止データ保護システムによって自動的に行われる偽造防止チェックを満たす必要があります。 テストで POST 要求を実行するには、テスト アプリで次のことを行う必要があります。
- ページに対して要求を行います。
- 応答の偽造防止 cookie と要求検証トークンを解析します。
- 偽造防止 cookie と要求検証トークンを使用して POST 要求を行います。
サンプル アプリの SendAsync
ヘルパー拡張メソッド (Helpers/HttpClientExtensions.cs
) と GetDocumentAsync
ヘルパー メソッド (Helpers/HtmlHelpers.cs
) は、次のメソッドで AngleSharp パーサーを使用して偽造防止チェック処理を行います。
GetDocumentAsync
: HttpResponseMessage を受け取り、IHtmlDocument
を返します。GetDocumentAsync
は、元のHttpResponseMessage
に基づいて仮想応答を準備するファクトリを使用します。 詳しくは、AngleSharp のドキュメントをご覧ください。HttpClient
のSendAsync
拡張メソッドを使って HttpRequestMessage を作成し、SendAsync(HttpRequestMessage) を呼び出して SUT に要求を送信します。SendAsync
のオーバーロードは、HTML フォーム (IHtmlFormElement
) と次のものを受け入れます。- フォームの送信ボタン (
IHtmlElement
) - フォームの値コレクション (
IEnumerable<KeyValuePair<string, string>>
) - 送信ボタン (
IHtmlElement
) とフォームの値 (IEnumerable<KeyValuePair<string, string>>
)
- フォームの送信ボタン (
AngleSharp は、この記事とサンプル アプリのデモンストレーションのために使用するサードパーティ製の解析ライブラリです。 ASP.NET Core アプリの統合テストでは、AngleSharp はサポートされていないか、必要ありません。 Html Agility Pack (HAP) などの他のパーサーを使用することもできます。 もう 1 つの方法として、偽造防止システムの要求検証トークンを処理するコードを記述し、偽造防止 cookie を直接処理する方法もあります。 詳細については、この記事の「偽造防止チェックのための AngleSharp と Application Parts
」を参照してください。
EF-Core インメモリ データベース プロバイダーは、限定された基本的なテストに使用できます。一方、"SQLite プロバイダーは、インメモリ テストに推奨される選択肢です"。
「スタートアップ フィルターを使用した Startup の拡張」をご覧ください。テストにカスタムのサービスやミドルウェアを必要とするときに便利な、IStartupFilter を使用したミドルウェアの構成方法がわかります。
WithWebHostBuilder を使用したクライアントのカスタマイズ
テスト メソッド内で追加の構成が必要な場合、WithWebHostBuilder では、IWebHostBuilder を使用して新しい WebApplicationFactory
を作成できます。
このサンプル コードは WithWebHostBuilder
を呼び出し、構成されたサービスをテスト スタブに置き換えます。 詳細と使用例については、この記事の「モック サービスの注入」を参照してください。
サンプル アプリ の Post_DeleteMessageHandler_ReturnsRedirectToRoot
テスト メソッドは、WithWebHostBuilder
の使用方法を示しています。 このテストでは、SUT からフォーム送信をトリガーすることによって、データベース内のレコードの削除を実行します。
IndexPageTests
クラス内の別のテストは、データベース内のすべてのレコードを削除する操作を実行します。この操作が Post_DeleteMessageHandler_ReturnsRedirectToRoot
メソッドの前に実行される可能性があるため、このテスト メソッド内でデータベースの再シード処理を行い、確実に SUT が削除するレコードが存在するようにしています。 SUT に対する要求では、SUT の messages
フォームの最初の [削除] ボタンの選択がシミュレートされます。
[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
// Arrange
using (var scope = _factory.Services.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var db = scopedServices.GetRequiredService<ApplicationDbContext>();
Utilities.ReinitializeDbForTests(db);
}
var defaultPage = await _client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
//Act
var response = await _client.SendAsync(
(IHtmlFormElement)content.QuerySelector("form[id='messages']"),
(IHtmlButtonElement)content.QuerySelector("form[id='messages']")
.QuerySelector("div[class='panel-body']")
.QuerySelector("button"));
// Assert
Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("/", response.Headers.Location.OriginalString);
}
クライアントのオプション
HttpClient
インスタンスを作成するときの既定値と使用可能なオプションについては、WebApplicationFactoryClientOptions に関するページを参照してください。
WebApplicationFactoryClientOptions
クラスを作成し、CreateClient() メソッドに渡します。
public class IndexPageTests :
IClassFixture<CustomWebApplicationFactory<Program>>
{
private readonly HttpClient _client;
private readonly CustomWebApplicationFactory<Program>
_factory;
public IndexPageTests(
CustomWebApplicationFactory<Program> factory)
{
_factory = factory;
_client = factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
}
"注": HTTPS リダイレクト ミドルウェアを使用する場合に、HTTPS リダイレクト警告のログを回避するには BaseAddress = new Uri("https://localhost")
を設定します
モック サービスの注入
ホスト ビルダーで ConfigureTestServices を呼び出すと、テストでサービスをオーバーライドできます。 オーバーライドされたサービスのスコープをテスト自体に設定するには、WithWebHostBuilder メソッドを使用してホスト ビルダーを取得します。 これは次のテストで確認できます。
- Get_QuoteService_ProvidesQuoteInPage
- Get_GithubProfilePageCanGetAGithubUser
- Get_SecurePageIsReturnedForAnAuthenticatedUser
サンプルの SUT には、引用符を返すスコープ サービスが含まれています。 インデックス ページが要求されると、インデックス ページの非表示フィールドに引用符が埋め込まれます。
Services/IQuoteService.cs
:
public interface IQuoteService
{
Task<string> GenerateQuote();
}
Services/QuoteService.cs
:
// Quote ©1975 BBC: The Doctor (Tom Baker); Dr. Who: Planet of Evil
// https://www.bbc.co.uk/programmes/p00pyrx6
public class QuoteService : IQuoteService
{
public Task<string> GenerateQuote()
{
return Task.FromResult<string>(
"Come on, Sarah. We've an appointment in London, " +
"and we're already 30,000 years late.");
}
}
Program.cs
:
services.AddScoped<IQuoteService, QuoteService>();
Pages/Index.cshtml.cs
:
public class IndexModel : PageModel
{
private readonly ApplicationDbContext _db;
private readonly IQuoteService _quoteService;
public IndexModel(ApplicationDbContext db, IQuoteService quoteService)
{
_db = db;
_quoteService = quoteService;
}
[BindProperty]
public Message Message { get; set; }
public IList<Message> Messages { get; private set; }
[TempData]
public string MessageAnalysisResult { get; set; }
public string Quote { get; private set; }
public async Task OnGetAsync()
{
Messages = await _db.GetMessagesAsync();
Quote = await _quoteService.GenerateQuote();
}
Pages/Index.cs
:
<input id="quote" type="hidden" value="@Model.Quote">
次のマークアップは、SUT アプリの実行時に生成されます。
<input id="quote" type="hidden" value="Come on, Sarah. We've an appointment in
London, and we're already 30,000 years late.">
統合テストでサービスと引用符の注入をテストするため、テストは SUT にモック サービスを注入します。 モック サービスは、アプリの QuoteService
をテスト アプリが提供する TestQuoteService
と呼ばれるサービスに置き換えます。
IntegrationTests.IndexPageTests.cs
:
// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
public Task<string> GenerateQuote()
{
return Task.FromResult(
"Something's interfering with time, Mr. Scarman, " +
"and time is my business.");
}
}
ConfigureTestServices
が呼び出され、スコープ サービスが登録されます。
[Fact]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddScoped<IQuoteService, TestQuoteService>();
});
})
.CreateClient();
//Act
var defaultPage = await client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
var quoteElement = content.QuerySelector("#quote");
// Assert
Assert.Equal("Something's interfering with time, Mr. Scarman, " +
"and time is my business.", quoteElement.Attributes["value"].Value);
}
TestQuoteService
によって指定された引用符テキストがテストの実行中に生成されたマークアップに反映されるため、アサーションは成功します。
<input id="quote" type="hidden" value="Something's interfering with time,
Mr. Scarman, and time is my business.">
モック認証
AuthTests
クラスのテストは、セキュリティで保護されたエンドポイントであることを確認します。
- 認証されていないユーザーは、アプリのサインイン ページにリダイレクトされます。
- 認証されたユーザーには、コンテンツを返します。
SUT の /SecurePage
ページでは、AuthorizePage 規約を使用してページに AuthorizeFilter を適用します。 詳細については、Razor Pages の承認規則に関するページを参照してください。
services.AddRazorPages(options =>
{
options.Conventions.AuthorizePage("/SecurePage");
});
Get_SecurePageRedirectsAnUnauthenticatedUser
テストでは、AllowAutoRedirect を false
に設定することで、WebApplicationFactoryClientOptions がリダイレクトを許可しないように設定しています。
[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
// Arrange
var client = _factory.CreateClient(
new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
// Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.StartsWith("http://localhost/Identity/Account/Login",
response.Headers.Location.OriginalString);
}
クライアントがリダイレクトに従うことを許可しないことで、次のチェックを行うことができます。
- SUT によって返される状態コードを確認するには、サインイン ページにリダイレクトした後の最終的な状態コード (これは HttpStatusCode.OK になります) ではなく、予期される HttpStatusCode.Redirect の結果と照らし合わせます。
- 最終的なサインイン ページ応答 (ここでは、
Location
ヘッダーは存在しません) ではなく、応答ヘッダーのLocation
ヘッダー値がhttp://localhost/Identity/Account/Login
で始まることを確認できます。
テスト アプリでは、認証と承認の側面をテストするために ConfigureTestServices の AuthenticationHandler<TOptions> をモックすることができます。 最小のシナリオでは、AuthenticateResult.Success が返されます。
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "TestScheme");
var result = AuthenticateResult.Success(ticket);
return Task.FromResult(result);
}
}
TestAuthHandler
は、認証スキームが TestScheme
に設定されている場合 (この場合、AddAuthentication
が ConfigureTestServices
に登録されています) に、ユーザーを認証するために呼び出されます。 アプリから要求されるスキームと TestScheme
スキームを一致させることが重要です。 そうでない場合は、認証が機能しません。
[Fact]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddAuthentication(defaultScheme: "TestScheme")
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
"TestScheme", options => { });
});
})
.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false,
});
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue(scheme: "TestScheme");
//Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
WebApplicationFactoryClientOptions
の詳細については、「クライアントのオプション」セクションを参照してください。
認証ミドルウェアの基本的なテスト
認証ミドルウェアの基本的なテストについては、こちらの GitHub リポジトリを参照してください。 これには、テスト シナリオに固有のテスト サーバーが含まれています。
環境を設定する
カスタム アプリケーション ファクトリで環境を設定します。
public class CustomWebApplicationFactory<TProgram>
: WebApplicationFactory<TProgram> where TProgram : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
var dbContextDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbContextOptions<ApplicationDbContext>));
services.Remove(dbContextDescriptor);
var dbConnectionDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbConnection));
services.Remove(dbConnectionDescriptor);
// Create open SqliteConnection so EF won't automatically close it.
services.AddSingleton<DbConnection>(container =>
{
var connection = new SqliteConnection("DataSource=:memory:");
connection.Open();
return connection;
});
services.AddDbContext<ApplicationDbContext>((container, options) =>
{
var connection = container.GetRequiredService<DbConnection>();
options.UseSqlite(connection);
});
});
builder.UseEnvironment("Development");
}
}
テスト インフラストラクチャがアプリ コンテンツのルート パスを推測する方法
WebApplicationFactory
コンストラクターを使用すると、TEntryPoint
アセンブリの System.Reflection.Assembly.FullName
と同じキーを持つ統合テストを含むアセンブリで WebApplicationFactoryContentRootAttribute を検索することによって、アプリのコンテンツ ルート パスを推測できます。 正しいキーを持つ属性が見つからない場合、WebApplicationFactory
はフォールバックしてソリューション ファイル ( .sln) を検索し、TEntryPoint
アセンブリ名をソリューション ディレクトリに追加します。 アプリのルート ディレクトリ (コンテンツ ルート パス) は、ビューやコンテンツのファイルを検出するために使用されます。
シャドウ コピーの無効化
シャドウ コピーを行うと、テストが出力ディレクトリとは異なるディレクトリで実行されます。 テストが Assembly.Location
に関連するファイルの読み込みに依存していて、問題が発生した場合、シャドウ コピーを無効にする必要がある場合があります。
xUnit を使用しているときにシャドウ コピーを無効にするには、正しい構成設定を使用して、テスト プロジェクト ディレクトリに xunit.runner.json
ファイルを作成します。
{
"shadowCopy": false
}
オブジェクトの破棄
IClassFixture
実装のテストを実行した後、xUnit によって を破棄すると、TestServer と WebApplicationFactory
HttpClient も破棄されます。 開発者がインスタンス化したオブジェクトを破棄する必要がある場合は、IClassFixture
の実装で破棄します。 詳細については、「Dispose メソッドの実装」を参照してください。
統合テストのサンプル
サンプル アプリは、次の 2 つのアプリで構成されています。
アプリ | プロジェクト ディレクトリ | 説明 |
---|---|---|
メッセージ アプリ (SUT) | src/RazorPagesProject |
ユーザーは、メッセージの追加、1 つ削除、すべて削除、および分析を行うことができます。 |
アプリをテストする | tests/RazorPagesProject.Tests |
SUT の統合テストに使用されます。 |
テストは、Visual Studio などの IDE に組み込まれているテスト機能を使用して実行できます。 Visual Studio Code またはコマンド ラインを使用している場合は、コマンド プロンプトで tests/RazorPagesProject.Tests
ディレクトリを開き、次のコマンドを実行します。
dotnet test
メッセージ アプリ (SUT) の構成
SUT は、次の特性を持つ Razor Pages メッセージ システムです。
- アプリのインデックス ページ (
Pages/Index.cshtml
とPages/Index.cshtml.cs
) には、メッセージの追加、削除、および分析 (メッセージあたりの平均単語数) を制御する UI およびページ モデル メソッドが用意されています。 - メッセージは、
Id
(キー) とText
(メッセージ) の 2 つのプロパティを持つMessage
クラス (Data/Message.cs
) によって記述されます。Text
プロパティは必須であり、200 文字までに制限されています。 - メッセージは、Entity Framework のメモリ内データベース† を使用して格納されます。
- アプリのデータベース コンテキスト クラスである
AppDbContext
(Data/AppDbContext.cs
) には、データアクセス層 (DAL) が含まれています。 - アプリの起動時にデータベースが空の場合、メッセージ ストアが 3 つのメッセージで初期化されます。
- アプリには、認証されたユーザーのみがアクセスできる
/SecurePage
が含まれています。
†InMemory を使用したテストに関する EF 記事では、MSTest を使用したテストでメモリ内データベースを使用する方法について説明します。 このトピックでは、xUnit テスト フレームワークを使用します。 テストの概念とテストの実装は、異なるテスト フレームワークでも類似していますが、同一ではありません。
アプリはリポジトリ パターンを使用しておらず、Unit of Work (UoW) パターンの有効な例ではありませんが、Razor Pages ではこれらの開発パターンがサポートされています。 詳細については、インフラストラクチャの永続化レイヤーの設計およびコントローラー ロジックのテストに関する記事を参照してください (このサンプルはリポジトリ パターンを実装しています)。
テスト アプリの構成
テスト アプリは、tests/RazorPagesProject.Tests
ディレクトリにあるコンソール アプリです。
テスト アプリのディレクトリ | 説明 |
---|---|
AuthTests |
次のテスト メソッドを含みます。
|
BasicTests |
ルーティングおよびコンテンツ タイプのテスト メソッドを含みます。 |
IntegrationTests |
カスタム WebApplicationFactory クラスを使用したインデックス ページの統合テストを含みます。 |
Helpers/Utilities |
|
テスト フレームワークは、xUnit です。 統合テストは、TestServer を含む Microsoft.AspNetCore.TestHost を使用して実行されます。 Microsoft.AspNetCore.Mvc.Testing
パッケージはテスト ホストとテスト サーバーを構成するために使用されるため、テスト アプリのプロジェクト ファイルまたはテスト アプリの開発者構成で直接 TestHost
および TestServer
パッケージを参照する必要はありません。
統合テストでは、通常、テストを実行する前に、データベース内に小さなデータセットが必要です。 たとえば、削除テストでは、データベース レコードの削除を呼び出します。そのため、削除要求を成功させるには、データベースに少なくとも 1 つのレコードが必要です。
このサンプル アプリでは、Utilities.cs
で 3 つのメッセージを使用してデータベースをシードします。このメッセージは、テストを実行する際に使用することができます。
public static void InitializeDbForTests(ApplicationDbContext db)
{
db.Messages.AddRange(GetSeedingMessages());
db.SaveChanges();
}
public static void ReinitializeDbForTests(ApplicationDbContext db)
{
db.Messages.RemoveRange(db.Messages);
InitializeDbForTests(db);
}
public static List<Message> GetSeedingMessages()
{
return new List<Message>()
{
new Message(){ Text = "TEST RECORD: You're standing on my scarf." },
new Message(){ Text = "TEST RECORD: Would you like a jelly baby?" },
new Message(){ Text = "TEST RECORD: To the rational mind, " +
"nothing is inexplicable; only unexplained." }
};
}
SUT のデータベース コンテキストは、Program.cs
に登録されます。 テスト アプリの builder.ConfigureServices
コールバックは、アプリの Program.cs
コードが実行された後に実行されます。 テストに異なるデータベースを使用するには、アプリのデータベース コンテキストを builder.ConfigureServices
で置き換える必要があります。 詳細については、「WebApplicationFactory のカスタマイズ」セクションをご覧ください。
その他の技術情報
ASP.NET Core