ASP.NET Core での Razor Pages の単体テスト

ASP.NET Core では Razor Pages アプリの単体テストをサポートしています。 データ アクセス層 (DAL) とページ モデルのテストによって、次のことを保証できます。

  • Razor Pages アプリの一部は、アプリの構築時に独立して、または 1 つの単位として連携して機能します。
  • クラスとメソッドの担当のスコープは限定されています。
  • アプリの動作に関する追加のドキュメントがあります。
  • コードの更新によって発生したエラーである回帰は、自動ビルドおよびデプロイ時に検出されます。

このトピックでは、Razor Pages アプリと単体テストの基本的な知識があることを前提としています。 Razor Pages アプリまたはテストの概念になじみがない場合は、次のトピックを参照してください。

サンプル コードを表示またはダウンロードします (ダウンロード方法)。

サンプル プロジェクトは、次の 2 つのアプリで構成されています。

アプリ プロジェクト フォルダー 説明
メッセージ アプリ src/RazorPagesTestSample ユーザーがメッセージの追加、1 つのメッセージの削除、すべてのメッセージの削除、およびメッセージの分析 (メッセージあたりの平均単語数の検出) をできるようにします。
アプリをテストする tests/RazorPagesTestSample.Tests メッセージ アプリの DAL およびインデックス ページ モデルの単体テストに使用されます。

テストは、Visual Studio などの IDE に組み込まれているテスト機能を使用して実行できます。 Visual Studio Code またはコマンド ラインを使用する場合は、コマンドプロンプトで tests/RazorPagesTestSample.Tests フォルダー内の次のコマンドを実行します。

dotnet test

メッセージ アプリの構成

メッセージ アプリは、次の特性を持つ Razor Pages メッセージ システムです。

  • アプリのインデックス ページ (Pages/Index.cshtmlPages/Index.cshtml.cs) には、メッセージの追加、削除、および分析 (メッセージあたりの平均単語数の検出) を制御する UI およびページ モデル メソッドが用意されています。
  • メッセージは、Id (キー) と Text (メッセージ) の 2 つのプロパティを持つ Message クラス (Data/Message.cs) によって記述されます。 Text プロパティは必須であり、200 文字までに制限されています。
  • メッセージは、Entity Framework のメモリ内データベース† を使用して格納されます。
  • アプリでは、データベース コンテキスト クラス AppDbContext (Data/AppDbContext.cs) 内に DAL を格納します。 DAL メソッドは virtual とマークされ、これにより、テストで使用するためのメソッドのモックを作成できます。
  • アプリの起動時にデータベースが空の場合、メッセージ ストアが 3 つのメッセージで初期化されます。 これらのシードされたメッセージも、テストで使用されます。

InMemory を使用したテストに関する EF トピックでは、MSTest を使用したテストでメモリ内データベースを使用する方法について説明しています。 このトピックでは、xUnit テスト フレームワークを使用します。 テストの概念とテストの実装は、異なるテスト フレームワークでも類似していますが、同一ではありません。

サンプル アプリではリポジトリ パターンを使用しておらず、Unit of Work (UoW) パターンの有効な例ではありませんが、Razor Pages ではこれらの開発パターンをサポートしています。 詳細については、「インフラストラクチャの永続レイヤーの設計」およびASP.NET Core でのコントローラー ロジックのテストに関する記事を参照してください (このサンプルでは、リポジトリ パターンを実装しています)。

テスト アプリの構成

テスト アプリは、tests/RazorPagesTestSample.Tests フォルダー内のコンソール アプリです。

テスト アプリ フォルダー 説明
UnitTests
  • DataAccessLayerTest.cs には、DAL の単体テストが含まれています。
  • IndexPageTests.cs には、インデックス ページ モデルの単体テストが含まれています。
Utilities データベースが各テストのベースライン条件にリセットされるように、各 DAL 単体テストの新しいデータベース コンテキスト オプションを作成するために使用する TestDbContextOptions メソッドが含まれています。

テスト フレームワークは、xUnit です。 オブジェクトのモック フレームワークは Moq です。

データアクセス層 (DAL) の単体テスト

メッセージ アプリには、AppDbContext クラス (src/RazorPagesTestSample/Data/AppDbContext.cs) に含まれる 4 つのメソッドを持つ DAL があります。 テスト アプリで、各メソッドには 1 つか 2 つの単体テストがあります。

DAL メソッド 関数
GetMessagesAsync データベースから Text プロパティで並べ替えられた List<Message> を取得します。
AddMessageAsync Message をデータベースに追加します。
DeleteAllMessagesAsync データベースからすべての Message エントリを削除します。
DeleteMessageAsync Id によって、データベースから 1 つの Message を削除します。

DAL の単体テストでは、各テストの新しい AppDbContext を作成する際に、DbContextOptions が必要です。 各テストの DbContextOptions を作成する 1 つの方法として、DbContextOptionsBuilder を使用します。

var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>()
    .UseInMemoryDatabase("InMemoryDb");

using (var db = new AppDbContext(optionsBuilder.Options))
{
    // Use the db here in the unit test.
}

この方法の問題は、各テストでは、前のテストでデータベースがどのような状態になっていても、それを受け取るということです。 相互に干渉しないアトミック単体テストを作成しようとする場合に、これが問題になる可能性があります。 AppDbContext でテストごとに新しいデータベース コンテキストを強制的に使用させるには、新しいサービス プロバイダーに基づいた DbContextOptions インスタンスを指定します。 テスト アプリは、その Utilities クラス メソッド TestDbContextOptions (tests/RazorPagesTestSample.Tests/Utilities/Utilities.cs) を使用してこれを行う方法を示しています。

public static DbContextOptions<AppDbContext> TestDbContextOptions()
{
    // Create a new service provider to create a new in-memory database.
    var serviceProvider = new ServiceCollection()
        .AddEntityFrameworkInMemoryDatabase()
        .BuildServiceProvider();

    // Create a new options instance using an in-memory database and 
    // IServiceProvider that the context should resolve all of its 
    // services from.
    var builder = new DbContextOptionsBuilder<AppDbContext>()
        .UseInMemoryDatabase("InMemoryDb")
        .UseInternalServiceProvider(serviceProvider);

    return builder.Options;
}

DAL 単体テストで DbContextOptions を使用すると、新しいデータベース インスタンスを使用して、各テストをアトミックに実行できます。

using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
{
    // Use the db here in the unit test.
}

DataAccessLayerTest クラスの各テスト メソッド (UnitTests/DataAccessLayerTest.cs) は、同様の Arrange-Act-Assert パターンに従います。

  1. Arrange (環境構築):データベースがテスト用に構成され、期待される結果が定義されています。
  2. Act (実行):テストが実行されます。
  3. Assert (動作確認):アサーションによって、テスト結果が成功であるかどうかが判断されます。

たとえば、DeleteMessageAsync メソッドは、その Id によって識別された 1 つのメッセージの削除を担当します (src/RazorPagesTestSample/Data/AppDbContext.cs)。

public async virtual Task DeleteMessageAsync(int id)
{
    var message = await Messages.FindAsync(id);

    if (message != null)
    {
        Messages.Remove(message);
        await SaveChangesAsync();
    }
}

このメソッドには 2 つのテストがあります。 1 つのテストでは、メッセージがデータベースに存在する場合に、このメソッドによってメッセージが削除されます。 他方のメソッドでは、削除対象のメッセージ Id が存在しない場合に、データベースが変更されないことをテストします。 DeleteMessageAsync_MessageIsDeleted_WhenMessageIsFound メソッドを次に示します。

[Fact]
public async Task DeleteMessageAsync_MessageIsDeleted_WhenMessageIsFound()
{
    using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
    {
        // Arrange
        var seedMessages = AppDbContext.GetSeedingMessages();
        await db.AddRangeAsync(seedMessages);
        await db.SaveChangesAsync();
        var recId = 1;
        var expectedMessages = 
            seedMessages.Where(message => message.Id != recId).ToList();

        // Act
        await db.DeleteMessageAsync(recId);

        // Assert
        var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
        Assert.Equal(
            expectedMessages.OrderBy(m => m.Id).Select(m => m.Text), 
            actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
    }
}

まず、このメソッドでは、Arrange ステップを実行し、Act ステップの準備を行います。 シード処理メッセージが取得され、seedMessages で保持されます。 シード処理メッセージはデータベースに保存されます。 Id1 のメッセージは、削除対象として設定されます。 DeleteMessageAsync メソッドが実行されると、予期されるメッセージには、Id1 のメッセージを除くすべてのメッセージが含まれるはずです。 expectedMessages 変数は、この予期される結果を表します。

// Arrange
var seedMessages = AppDbContext.GetSeedingMessages();
await db.AddRangeAsync(seedMessages);
await db.SaveChangesAsync();
var recId = 1;
var expectedMessages = 
    seedMessages.Where(message => message.Id != recId).ToList();

メソッドは次のように動作します。DeleteMessageAsync メソッドは 1recId を渡して実行されます。

// Act
await db.DeleteMessageAsync(recId);

最後に、メソッドはコンテキストから Messages を取得し、expectedMessages と比較して、2 つが等しいことをアサートします。

// Assert
var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
Assert.Equal(
    expectedMessages.OrderBy(m => m.Id).Select(m => m.Text), 
    actualMessages.OrderBy(m => m.Id).Select(m => m.Text));

2 つの List<Message> が同じであることを比較するために:

  • メッセージは Id 順に並べ替えられます。
  • メッセージのペアは、Text プロパティで比較されます。

類似のテスト メソッド DeleteMessageAsync_NoMessageIsDeleted_WhenMessageIsNotFound では、存在しないメッセージを削除しようとした結果を確認します。 この場合、データベース内の予期されるメッセージは、DeleteMessageAsync メソッドが実行された後の実際のメッセージと等しくなるはずです。 データベースのコンテンツへの変更はないはずです。

[Fact]
public async Task DeleteMessageAsync_NoMessageIsDeleted_WhenMessageIsNotFound()
{
    using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
    {
        // Arrange
        var expectedMessages = AppDbContext.GetSeedingMessages();
        await db.AddRangeAsync(expectedMessages);
        await db.SaveChangesAsync();
        var recId = 4;

        // Act
        try
        {
            await db.DeleteMessageAsync(recId);
        }
        catch
        {
            // recId doesn't exist
        }

        // Assert
        var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
        Assert.Equal(
            expectedMessages.OrderBy(m => m.Id).Select(m => m.Text), 
            actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
    }
}

ページ モデル メソッドの単体テスト

別の単体テスト セットは、ページ モデル メソッドのテストを担当します。 メッセージ アプリで、インデックス ページ モデルは、src/RazorPagesTestSample/Pages/Index.cshtml.csIndexModel クラスにあります。

ページ モデル メソッド 関数
OnGetAsync GetMessagesAsync メソッドを使用して、UI の DAL からメッセージを取得します。
OnPostAddMessageAsync ModelState が有効な場合、AddMessageAsync を呼び出して、データベースにメッセージを追加します。
OnPostDeleteAllMessagesAsync データベース内のすべてのメッセージを削除するには、DeleteAllMessagesAsync を呼び出します。
OnPostDeleteMessageAsync Id を指定してメッセージを削除するには、DeleteMessageAsync を実行します。
OnPostAnalyzeMessagesAsync データベースに 1 つ以上のメッセージが含まれている場合、メッセージあたりの平均単語数を計算します。

ページ モデル メソッドは、IndexPageTests クラス (tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs) の 7 つのテストを使用してテストされます。 テストでは、おなじみの Arrange-Assert-Act パターンを使用します。 これらのテストでは次に焦点を合わせています。

  • ModelState が無効な場合に、メソッドが正しい動作に従うかどうかを判断します。
  • メソッドによって正しい IActionResult が生成されることを確認します。
  • プロパティ値の割り当てが正しく行われていることを確認します。

この一連のテストでは、多くの場合に、ページ モデル メソッドが実行される Act ステップ用に予期されるデータを生成するために、DAL のメソッドのモックを作成します。 たとえば、AppDbContextGetMessagesAsync メソッドは、出力を生成するためにモックが作成されます。 ページ モデル メソッドでこのメソッドを実行すると、モックによって結果が返されます。 データはデータベースから取得されません。 これにより、ページ モデル テストで DAL を使用するための、予測可能で信頼性の高いテスト条件が作成されます。

OnGetAsync_PopulatesThePageModel_WithAListOfMessages テストでは、ページ モデルに対して GetMessagesAsync メソッドのモックを作成する方法を示しています。

var mockAppDbContext = new Mock<AppDbContext>(optionsBuilder.Options);
var expectedMessages = AppDbContext.GetSeedingMessages();
mockAppDbContext.Setup(
    db => db.GetMessagesAsync()).Returns(Task.FromResult(expectedMessages));
var pageModel = new IndexModel(mockAppDbContext.Object);

Act ステップで OnGetAsync メソッドが実行されると、ページ モデルの GetMessagesAsync メソッドが呼び出されます。

単体テストの Act ステップ (tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs):

// Act
await pageModel.OnGetAsync();

IndexPage ページ モデルの OnGetAsync メソッド (src/RazorPagesTestSample/Pages/Index.cshtml.cs):

public async Task OnGetAsync()
{
    Messages = await _db.GetMessagesAsync();
}

DAL の GetMessagesAsync メソッドでは、このメソッド呼び出しの結果を返しません。 メソッドのモック バージョンで、結果を返します。

Assert ステップでは、実際のメッセージ (actualMessages) がページ モデルの Messages プロパティから割り当てられます。 メッセージが割り当てられるときに、型チェックも実行されます。 予期されるメッセージと実際のメッセージが、それらの Text プロパティによって比較されます。 テストでは、2 つの List<Message> インスタンスに同じメッセージが含まれていることをアサートします。

// Assert
var actualMessages = Assert.IsAssignableFrom<List<Message>>(pageModel.Messages);
Assert.Equal(
    expectedMessages.OrderBy(m => m.Id).Select(m => m.Text), 
    actualMessages.OrderBy(m => m.Id).Select(m => m.Text));

このグループの他のテストでは、DefaultHttpContextModelStateDictionaryPageContext を確立するための ActionContextViewDataDictionary、および PageContext を含むページ モデル オブジェクトが作成されます。 これらは、テストの実施に役立ちます。 たとえば、メッセージ アプリでは、OnPostAddMessageAsync の実行時に有効な PageResult が返されることを確認するために、AddModelError を使用して ModelState エラーを作成します。

[Fact]
public async Task OnPostAddMessageAsync_ReturnsAPageResult_WhenModelStateIsInvalid()
{
    // Arrange
    var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>()
        .UseInMemoryDatabase("InMemoryDb");
    var mockAppDbContext = new Mock<AppDbContext>(optionsBuilder.Options);
    var expectedMessages = AppDbContext.GetSeedingMessages();
    mockAppDbContext.Setup(db => db.GetMessagesAsync()).Returns(Task.FromResult(expectedMessages));
    var httpContext = new DefaultHttpContext();
    var modelState = new ModelStateDictionary();
    var actionContext = new ActionContext(httpContext, new RouteData(), new PageActionDescriptor(), modelState);
    var modelMetadataProvider = new EmptyModelMetadataProvider();
    var viewData = new ViewDataDictionary(modelMetadataProvider, modelState);
    var tempData = new TempDataDictionary(httpContext, Mock.Of<ITempDataProvider>());
    var pageContext = new PageContext(actionContext)
    {
        ViewData = viewData
    };
    var pageModel = new IndexModel(mockAppDbContext.Object)
    {
        PageContext = pageContext,
        TempData = tempData,
        Url = new UrlHelper(actionContext)
    };
    pageModel.ModelState.AddModelError("Message.Text", "The Text field is required.");

    // Act
    var result = await pageModel.OnPostAddMessageAsync();

    // Assert
    Assert.IsType<PageResult>(result);
}

その他の技術情報

ASP.NET Core では Razor Pages アプリの単体テストをサポートしています。 データ アクセス層 (DAL) とページ モデルのテストによって、次のことを保証できます。

  • Razor Pages アプリの一部は、アプリの構築時に独立して、または 1 つの単位として連携して機能します。
  • クラスとメソッドの担当のスコープは限定されています。
  • アプリの動作に関する追加のドキュメントがあります。
  • コードの更新によって発生したエラーである回帰は、自動ビルドおよびデプロイ時に検出されます。

このトピックでは、Razor Pages アプリと単体テストの基本的な知識があることを前提としています。 Razor Pages アプリまたはテストの概念になじみがない場合は、次のトピックを参照してください。

サンプル コードを表示またはダウンロードします (ダウンロード方法)。

サンプル プロジェクトは、次の 2 つのアプリで構成されています。

アプリ プロジェクト フォルダー 説明
メッセージ アプリ src/RazorPagesTestSample ユーザーがメッセージの追加、1 つのメッセージの削除、すべてのメッセージの削除、およびメッセージの分析 (メッセージあたりの平均単語数の検出) をできるようにします。
アプリをテストする tests/RazorPagesTestSample.Tests メッセージ アプリの DAL およびインデックス ページ モデルの単体テストに使用されます。

テストは、Visual Studio などの IDE に組み込まれているテスト機能を使用して実行できます。 Visual Studio Code またはコマンド ラインを使用する場合は、コマンドプロンプトで tests/RazorPagesTestSample.Tests フォルダー内の次のコマンドを実行します。

dotnet test

メッセージ アプリの構成

メッセージ アプリは、次の特性を持つ Razor Pages メッセージ システムです。

  • アプリのインデックス ページ (Pages/Index.cshtmlPages/Index.cshtml.cs) には、メッセージの追加、削除、および分析 (メッセージあたりの平均単語数の検出) を制御する UI およびページ モデル メソッドが用意されています。
  • メッセージは、Id (キー) と Text (メッセージ) の 2 つのプロパティを持つ Message クラス (Data/Message.cs) によって記述されます。 Text プロパティは必須であり、200 文字までに制限されています。
  • メッセージは、Entity Framework のメモリ内データベース† を使用して格納されます。
  • アプリでは、データベース コンテキスト クラス AppDbContext (Data/AppDbContext.cs) 内に DAL を格納します。 DAL メソッドは virtual とマークされ、これにより、テストで使用するためのメソッドのモックを作成できます。
  • アプリの起動時にデータベースが空の場合、メッセージ ストアが 3 つのメッセージで初期化されます。 これらのシードされたメッセージも、テストで使用されます。

InMemory を使用したテストに関する EF トピックでは、MSTest を使用したテストでメモリ内データベースを使用する方法について説明しています。 このトピックでは、xUnit テスト フレームワークを使用します。 テストの概念とテストの実装は、異なるテスト フレームワークでも類似していますが、同一ではありません。

サンプル アプリではリポジトリ パターンを使用しておらず、Unit of Work (UoW) パターンの有効な例ではありませんが、Razor Pages ではこれらの開発パターンをサポートしています。 詳細については、「インフラストラクチャの永続レイヤーの設計」およびASP.NET Core でのコントローラー ロジックのテストに関する記事を参照してください (このサンプルでは、リポジトリ パターンを実装しています)。

テスト アプリの構成

テスト アプリは、tests/RazorPagesTestSample.Tests フォルダー内のコンソール アプリです。

テスト アプリ フォルダー 説明
UnitTests
  • DataAccessLayerTest.cs には、DAL の単体テストが含まれています。
  • IndexPageTests.cs には、インデックス ページ モデルの単体テストが含まれています。
Utilities データベースが各テストのベースライン条件にリセットされるように、各 DAL 単体テストの新しいデータベース コンテキスト オプションを作成するために使用する TestDbContextOptions メソッドが含まれています。

テスト フレームワークは、xUnit です。 オブジェクトのモック フレームワークは Moq です。

データアクセス層 (DAL) の単体テスト

メッセージ アプリには、AppDbContext クラス (src/RazorPagesTestSample/Data/AppDbContext.cs) に含まれる 4 つのメソッドを持つ DAL があります。 テスト アプリで、各メソッドには 1 つか 2 つの単体テストがあります。

DAL メソッド 関数
GetMessagesAsync データベースから Text プロパティで並べ替えられた List<Message> を取得します。
AddMessageAsync Message をデータベースに追加します。
DeleteAllMessagesAsync データベースからすべての Message エントリを削除します。
DeleteMessageAsync Id によって、データベースから 1 つの Message を削除します。

DAL の単体テストでは、各テストの新しい AppDbContext を作成する際に、DbContextOptions が必要です。 各テストの DbContextOptions を作成する 1 つの方法として、DbContextOptionsBuilder を使用します。

var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>()
    .UseInMemoryDatabase("InMemoryDb");

using (var db = new AppDbContext(optionsBuilder.Options))
{
    // Use the db here in the unit test.
}

この方法の問題は、各テストでは、前のテストでデータベースがどのような状態になっていても、それを受け取るということです。 相互に干渉しないアトミック単体テストを作成しようとする場合に、これが問題になる可能性があります。 AppDbContext でテストごとに新しいデータベース コンテキストを強制的に使用させるには、新しいサービス プロバイダーに基づいた DbContextOptions インスタンスを指定します。 テスト アプリは、その Utilities クラス メソッド TestDbContextOptions (tests/RazorPagesTestSample.Tests/Utilities/Utilities.cs) を使用してこれを行う方法を示しています。

public static DbContextOptions<AppDbContext> TestDbContextOptions()
{
    // Create a new service provider to create a new in-memory database.
    var serviceProvider = new ServiceCollection()
        .AddEntityFrameworkInMemoryDatabase()
        .BuildServiceProvider();

    // Create a new options instance using an in-memory database and 
    // IServiceProvider that the context should resolve all of its 
    // services from.
    var builder = new DbContextOptionsBuilder<AppDbContext>()
        .UseInMemoryDatabase("InMemoryDb")
        .UseInternalServiceProvider(serviceProvider);

    return builder.Options;
}

DAL 単体テストで DbContextOptions を使用すると、新しいデータベース インスタンスを使用して、各テストをアトミックに実行できます。

using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
{
    // Use the db here in the unit test.
}

DataAccessLayerTest クラスの各テスト メソッド (UnitTests/DataAccessLayerTest.cs) は、同様の Arrange-Act-Assert パターンに従います。

  1. Arrange (環境構築):データベースがテスト用に構成され、期待される結果が定義されています。
  2. Act (実行):テストが実行されます。
  3. Assert (動作確認):アサーションによって、テスト結果が成功であるかどうかが判断されます。

たとえば、DeleteMessageAsync メソッドは、その Id によって識別された 1 つのメッセージの削除を担当します (src/RazorPagesTestSample/Data/AppDbContext.cs)。

public async virtual Task DeleteMessageAsync(int id)
{
    var message = await Messages.FindAsync(id);

    if (message != null)
    {
        Messages.Remove(message);
        await SaveChangesAsync();
    }
}

このメソッドには 2 つのテストがあります。 1 つのテストでは、メッセージがデータベースに存在する場合に、このメソッドによってメッセージが削除されます。 他方のメソッドでは、削除対象のメッセージ Id が存在しない場合に、データベースが変更されないことをテストします。 DeleteMessageAsync_MessageIsDeleted_WhenMessageIsFound メソッドを次に示します。

[Fact]
public async Task DeleteMessageAsync_MessageIsDeleted_WhenMessageIsFound()
{
    using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
    {
        // Arrange
        var seedMessages = AppDbContext.GetSeedingMessages();
        await db.AddRangeAsync(seedMessages);
        await db.SaveChangesAsync();
        var recId = 1;
        var expectedMessages = 
            seedMessages.Where(message => message.Id != recId).ToList();

        // Act
        await db.DeleteMessageAsync(recId);

        // Assert
        var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
        Assert.Equal(
            expectedMessages.OrderBy(m => m.Id).Select(m => m.Text), 
            actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
    }
}

まず、このメソッドでは、Arrange ステップを実行し、Act ステップの準備を行います。 シード処理メッセージが取得され、seedMessages で保持されます。 シード処理メッセージはデータベースに保存されます。 Id1 のメッセージは、削除対象として設定されます。 DeleteMessageAsync メソッドが実行されると、予期されるメッセージには、Id1 のメッセージを除くすべてのメッセージが含まれるはずです。 expectedMessages 変数は、この予期される結果を表します。

// Arrange
var seedMessages = AppDbContext.GetSeedingMessages();
await db.AddRangeAsync(seedMessages);
await db.SaveChangesAsync();
var recId = 1;
var expectedMessages = 
    seedMessages.Where(message => message.Id != recId).ToList();

メソッドは次のように動作します。DeleteMessageAsync メソッドは 1recId を渡して実行されます。

// Act
await db.DeleteMessageAsync(recId);

最後に、メソッドはコンテキストから Messages を取得し、expectedMessages と比較して、2 つが等しいことをアサートします。

// Assert
var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
Assert.Equal(
    expectedMessages.OrderBy(m => m.Id).Select(m => m.Text), 
    actualMessages.OrderBy(m => m.Id).Select(m => m.Text));

2 つの List<Message> が同じであることを比較するために:

  • メッセージは Id 順に並べ替えられます。
  • メッセージのペアは、Text プロパティで比較されます。

類似のテスト メソッド DeleteMessageAsync_NoMessageIsDeleted_WhenMessageIsNotFound では、存在しないメッセージを削除しようとした結果を確認します。 この場合、データベース内の予期されるメッセージは、DeleteMessageAsync メソッドが実行された後の実際のメッセージと等しくなるはずです。 データベースのコンテンツへの変更はないはずです。

[Fact]
public async Task DeleteMessageAsync_NoMessageIsDeleted_WhenMessageIsNotFound()
{
    using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
    {
        // Arrange
        var expectedMessages = AppDbContext.GetSeedingMessages();
        await db.AddRangeAsync(expectedMessages);
        await db.SaveChangesAsync();
        var recId = 4;

        // Act
        await db.DeleteMessageAsync(recId);

        // Assert
        var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
        Assert.Equal(
            expectedMessages.OrderBy(m => m.Id).Select(m => m.Text), 
            actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
    }
}

ページ モデル メソッドの単体テスト

別の単体テスト セットは、ページ モデル メソッドのテストを担当します。 メッセージ アプリで、インデックス ページ モデルは、src/RazorPagesTestSample/Pages/Index.cshtml.csIndexModel クラスにあります。

ページ モデル メソッド 関数
OnGetAsync GetMessagesAsync メソッドを使用して、UI の DAL からメッセージを取得します。
OnPostAddMessageAsync ModelState が有効な場合、AddMessageAsync を呼び出して、データベースにメッセージを追加します。
OnPostDeleteAllMessagesAsync データベース内のすべてのメッセージを削除するには、DeleteAllMessagesAsync を呼び出します。
OnPostDeleteMessageAsync Id を指定してメッセージを削除するには、DeleteMessageAsync を実行します。
OnPostAnalyzeMessagesAsync データベースに 1 つ以上のメッセージが含まれている場合、メッセージあたりの平均単語数を計算します。

ページ モデル メソッドは、IndexPageTests クラス (tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs) の 7 つのテストを使用してテストされます。 テストでは、おなじみの Arrange-Assert-Act パターンを使用します。 これらのテストでは次に焦点を合わせています。

  • ModelState が無効な場合に、メソッドが正しい動作に従うかどうかを判断します。
  • メソッドによって正しい IActionResult が生成されることを確認します。
  • プロパティ値の割り当てが正しく行われていることを確認します。

この一連のテストでは、多くの場合に、ページ モデル メソッドが実行される Act ステップ用に予期されるデータを生成するために、DAL のメソッドのモックを作成します。 たとえば、AppDbContextGetMessagesAsync メソッドは、出力を生成するためにモックが作成されます。 ページ モデル メソッドでこのメソッドを実行すると、モックによって結果が返されます。 データはデータベースから取得されません。 これにより、ページ モデル テストで DAL を使用するための、予測可能で信頼性の高いテスト条件が作成されます。

OnGetAsync_PopulatesThePageModel_WithAListOfMessages テストでは、ページ モデルに対して GetMessagesAsync メソッドのモックを作成する方法を示しています。

var mockAppDbContext = new Mock<AppDbContext>(optionsBuilder.Options);
var expectedMessages = AppDbContext.GetSeedingMessages();
mockAppDbContext.Setup(
    db => db.GetMessagesAsync()).Returns(Task.FromResult(expectedMessages));
var pageModel = new IndexModel(mockAppDbContext.Object);

Act ステップで OnGetAsync メソッドが実行されると、ページ モデルの GetMessagesAsync メソッドが呼び出されます。

単体テストの Act ステップ (tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs):

// Act
await pageModel.OnGetAsync();

IndexPage ページ モデルの OnGetAsync メソッド (src/RazorPagesTestSample/Pages/Index.cshtml.cs):

public async Task OnGetAsync()
{
    Messages = await _db.GetMessagesAsync();
}

DAL の GetMessagesAsync メソッドでは、このメソッド呼び出しの結果を返しません。 メソッドのモック バージョンで、結果を返します。

Assert ステップでは、実際のメッセージ (actualMessages) がページ モデルの Messages プロパティから割り当てられます。 メッセージが割り当てられるときに、型チェックも実行されます。 予期されるメッセージと実際のメッセージが、それらの Text プロパティによって比較されます。 テストでは、2 つの List<Message> インスタンスに同じメッセージが含まれていることをアサートします。

// Assert
var actualMessages = Assert.IsAssignableFrom<List<Message>>(pageModel.Messages);
Assert.Equal(
    expectedMessages.OrderBy(m => m.Id).Select(m => m.Text), 
    actualMessages.OrderBy(m => m.Id).Select(m => m.Text));

このグループの他のテストでは、DefaultHttpContextModelStateDictionaryPageContext を確立するための ActionContextViewDataDictionary、および PageContext を含むページ モデル オブジェクトが作成されます。 これらは、テストの実施に役立ちます。 たとえば、メッセージ アプリでは、OnPostAddMessageAsync の実行時に有効な PageResult が返されることを確認するために、AddModelError を使用して ModelState エラーを作成します。

[Fact]
public async Task OnPostAddMessageAsync_ReturnsAPageResult_WhenModelStateIsInvalid()
{
    // Arrange
    var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>()
        .UseInMemoryDatabase("InMemoryDb");
    var mockAppDbContext = new Mock<AppDbContext>(optionsBuilder.Options);
    var expectedMessages = AppDbContext.GetSeedingMessages();
    mockAppDbContext.Setup(db => db.GetMessagesAsync()).Returns(Task.FromResult(expectedMessages));
    var httpContext = new DefaultHttpContext();
    var modelState = new ModelStateDictionary();
    var actionContext = new ActionContext(httpContext, new RouteData(), new PageActionDescriptor(), modelState);
    var modelMetadataProvider = new EmptyModelMetadataProvider();
    var viewData = new ViewDataDictionary(modelMetadataProvider, modelState);
    var tempData = new TempDataDictionary(httpContext, Mock.Of<ITempDataProvider>());
    var pageContext = new PageContext(actionContext)
    {
        ViewData = viewData
    };
    var pageModel = new IndexModel(mockAppDbContext.Object)
    {
        PageContext = pageContext,
        TempData = tempData,
        Url = new UrlHelper(actionContext)
    };
    pageModel.ModelState.AddModelError("Message.Text", "The Text field is required.");

    // Act
    var result = await pageModel.OnPostAddMessageAsync();

    // Assert
    Assert.IsType<PageResult>(result);
}

その他の技術情報