RazorASP.NET Core 中的頁面單元測試

ASP.NET Core 支援的 Razor 單元測試頁面應用程式。 資料存取層 (DAL) 和頁面模型的測試有助於確保:

  • Pages 應用程式的各個部分在應用程式結構期間,會獨立地以一個單位來 Razor 運作。
  • 類別和方法的責任範圍有限。
  • 應用程式的行為方式有其他檔。
  • 在自動化的建立和部署期間,會發現回歸(這是程式碼更新所帶來的錯誤)。

本主題假設您對頁面應用程式和單元測試有基本的 Razor 瞭解。 如果您不熟悉 Razor 頁面應用程式或測試概念,請參閱下列主題:

查看或下載範例程式碼 (如何下載)

範例專案是由兩個應用程式所組成:

應用程式 Project 資料夾 描述
訊息應用程式 src/ Razor PagesTestSample 可讓使用者新增訊息、刪除一則訊息、刪除所有訊息,以及分析訊息 (尋找每個訊息的平均單字數目) 。
測試應用程式 測試/ Razor PagesTestSample 用來對訊息應用程式的 DAL 和索引頁面模型進行單元測試。

您可以使用 IDE 的內建測試功能來執行測試,例如Visual StudioVisual Studio for Mac。 如果使用Visual Studio Code或命令列,請在 [測試/ Razor PagesTestSample ] 資料夾的命令提示字元中執行下列命令:

dotnet test

訊息應用程式組織

訊息應用程式是 Razor 具有下列特性的頁面訊息系統:

  • 應用程式 (Pages/Index.cshtmlPages/Index.cshtml.cs) 的 [索引] 頁面會提供 UI 和頁面模型方法,以控制訊息的新增、刪除和分析, (尋找每個訊息) 的平均單字數目。
  • ) 有兩個屬性的類別 (Data/Message.cs 描述 Message 訊息: Id (索引鍵) 和 Text (訊息) 。 屬性是必要的 Text ,且限制為200個字元。
  • 訊息是使用 Entity Framework 的記憶體內部資料庫†儲存的。
  • 應用程式在其資料庫內容類別中包含 DAL, AppDbContext (Data/AppDbContext.cs) 。 DAL 方法會標示 virtual 為,可讓您模擬要在測試中使用的方法。
  • 如果應用程式啟動時資料庫是空的,則會使用三個訊息來初始化訊息存放區。 這些 入的訊息也會在測試中使用。

† EF 主題: 使用 InMemory 進行測試,說明如何使用記憶體內部資料庫來搭配 MSTest 進行測試。 本主題使用 xUnit 測試架構。 跨不同測試架構的測試概念和測試的執行方式類似,但不完全相同。

雖然範例應用程式不會使用存放庫模式,且不是 工作單元) (的有效範例, Razor 但頁面支援這些開發模式。 如需詳細資訊,請參閱 ASP.NET Core 中的設計基礎結構持續性層測試控制器邏輯 (範例會) 提供儲存機制模式。

測試應用程式組織

測試應用程式是 [ 測試/ Razor PagesTestSample ] 資料夾內的主控台應用程式。

測試應用程式資料夾 描述
UnitTests
  • DataAccessLayerTest.cs 包含 DAL 的單元測試。
  • IndexPageTests.cs 包含索引頁面模型的單元測試。
公用程式 TestDbContextOptions包含用來為每個 DAL 單元測試建立新資料庫內容選項的方法,以便將資料庫重設為每個測試的基準條件。

測試架構為 xUnit。 物件模擬架構為 Moq

資料存取層 (DAL 的單元測試)

訊息應用程式具有具有四個類別的 AppDbContext DAL, (src/RazorPagesTestSample/Data/AppDbContext.cs) 。 每個方法在測試應用程式中都有一個或兩個單元測試。

DAL 方法 函式
GetMessagesAsync List<Message>從資料庫取得,並依 Text 屬性排序。
AddMessageAsync Message將加入至資料庫。
DeleteAllMessagesAsync 從資料庫中刪除所有 Message 專案。
DeleteMessageAsync 從資料庫 Id 中刪除單一 Message

為每個測試建立新 AppDbContext 的時,DAL 需要 DbContextOptions 單元測試。 針對每個測試建立的 DbContextOptions 其中一種方法是使用 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.
}

類別 (UnitTests/DataAccessLayerTest.cs) 中 DataAccessLayerTest 的每個測試方法都遵循類似的排列-Act Assert 模式:

  1. 排列:資料庫設定為測試及/或定義預期的結果。
  2. Act:執行測試。
  3. 判斷提示:判斷測試結果是否成功的判斷提示。

例如, DeleteMessageAsync 方法會負責移除其 Id (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();
    }
}

這種方法有兩種測試。 當訊息存在於資料庫中時,一項測試會檢查此方法是否會刪除訊息。 另一種方法會測試如果刪除的訊息 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));
    }
}

首先,此方法會執行「排列」步驟,以進行「動作」步驟的準備。 系統會取得並保存植入 seedMessages 訊息。 植入訊息會儲存在資料庫中。 具有 Id 之的 1 訊息會設定為 [刪除]。 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 方法會傳遞 recId1 的:

// Act
await db.DeleteMessageAsync(recId);

最後,方法 Messages 會從內容中取得,並將它與 expectedMessages 判斷提示兩者相等的判斷提示比較:

// 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));

為了比較兩者 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));
    }
}

頁面模型方法的單元測試

另一組單元測試負責測試頁面模型方法。 在訊息應用程式中,索引頁面模型可在的 IndexModel 類別 src/RazorPagesTestSample/Pages/Index.cshtml.cs 中找到。

頁面模型方法 函式
OnGetAsync 使用 GetMessagesAsync 方法,從 DAL 取得適用于 UI 的訊息。
OnPostAddMessageAsync 如果 ModelState 有效,則呼叫 AddMessageAsync 會將訊息加入至資料庫。
OnPostDeleteAllMessagesAsync 呼叫 DeleteAllMessagesAsync 以刪除資料庫中的所有訊息。
OnPostDeleteMessageAsync 執行 DeleteMessageAsync 以刪除具有指定之的 Id 訊息。
OnPostAnalyzeMessagesAsync 如果資料庫中有一或多個訊息,則會計算每個訊息的平均單字數。

頁面模型方法會使用類別 (tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs) 中 IndexPageTests 的七個測試進行測試。 測試會使用熟悉的「排列-判斷提示-Act」模式。 這些測試著重于:

  • 判斷在 ModelState 無效時,方法是否遵循正確的行為。
  • 確認方法會產生正確 IActionResult 的。
  • 檢查是否已正確設定屬性值。

這組測試通常會模擬 DAL 的方法,為執行頁面模型方法的 Act 步驟產生預期的資料。 例如, GetMessagesAsync 的方法 AppDbContext 會模擬以產生輸出。 當頁面模型方法執行此方法時,mock 會傳回結果。 資料不是來自資料庫。 這會建立可預測、可靠的測試條件,以便在頁面模型測試中使用 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);

OnGetAsync當方法在 Act 步驟中執行時,它會呼叫頁面模型的 GetMessagesAsync 方法。

單元測試動作步驟 (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 屬性進行比較。 測試會判斷兩個 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));

此群組中的其他測試會建立頁面模型物件,這些物件包含 DefaultHttpContext 、、, ActionContext 以建立 PageContextViewDataDictionaryPageContextModelStateDictionary 。 這些都適用于進行測試。 例如,訊息應用程式會建立 ModelState 錯誤, AddModelError 以檢查執行時 OnPostAddMessageAsync 是否傳回有效 PageResult 的:

[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 單元測試頁面應用程式。 資料存取層 (DAL) 和頁面模型的測試有助於確保:

  • Pages 應用程式的各個部分在應用程式結構期間,會獨立地以一個單位來 Razor 運作。
  • 類別和方法的責任範圍有限。
  • 應用程式的行為方式有其他檔。
  • 在自動化的建立和部署期間,會發現回歸(這是程式碼更新所帶來的錯誤)。

本主題假設您對頁面應用程式和單元測試有基本的 Razor 瞭解。 如果您不熟悉 Razor 頁面應用程式或測試概念,請參閱下列主題:

查看或下載範例程式碼 (如何下載)

範例專案是由兩個應用程式所組成:

應用程式 Project 資料夾 描述
訊息應用程式 src/ Razor PagesTestSample 可讓使用者新增訊息、刪除一則訊息、刪除所有訊息,以及分析訊息 (尋找每個訊息的平均單字數目) 。
測試應用程式 測試/ Razor PagesTestSample 用來對訊息應用程式的 DAL 和索引頁面模型進行單元測試。

您可以使用 IDE 的內建測試功能來執行測試,例如Visual StudioVisual Studio for Mac。 如果使用Visual Studio Code或命令列,請在 [測試/ Razor PagesTestSample ] 資料夾的命令提示字元中執行下列命令:

dotnet test

訊息應用程式組織

訊息應用程式是 Razor 具有下列特性的頁面訊息系統:

  • 應用程式 (Pages/Index.cshtmlPages/Index.cshtml.cs) 的 [索引] 頁面會提供 UI 和頁面模型方法,以控制訊息的新增、刪除和分析, (尋找每個訊息) 的平均單字數目。
  • ) 有兩個屬性的類別 (Data/Message.cs 描述 Message 訊息: Id (索引鍵) 和 Text (訊息) 。 屬性是必要的 Text ,且限制為200個字元。
  • 訊息是使用 Entity Framework 的記憶體內部資料庫†儲存的。
  • 應用程式在其資料庫內容類別中包含 DAL, AppDbContext (Data/AppDbContext.cs) 。 DAL 方法會標示 virtual 為,可讓您模擬要在測試中使用的方法。
  • 如果應用程式啟動時資料庫是空的,則會使用三個訊息來初始化訊息存放區。 這些 入的訊息也會在測試中使用。

† EF 主題: 使用 InMemory 進行測試,說明如何使用記憶體內部資料庫來搭配 MSTest 進行測試。 本主題使用 xUnit 測試架構。 跨不同測試架構的測試概念和測試的執行方式類似,但不完全相同。

雖然範例應用程式不會使用存放庫模式,且不是 工作單元) (的有效範例, Razor 但頁面支援這些開發模式。 如需詳細資訊,請參閱 ASP.NET Core 中的設計基礎結構持續性層測試控制器邏輯 (範例會) 提供儲存機制模式。

測試應用程式組織

測試應用程式是 [ 測試/ Razor PagesTestSample ] 資料夾內的主控台應用程式。

測試應用程式資料夾 描述
UnitTests
  • DataAccessLayerTest.cs 包含 DAL 的單元測試。
  • IndexPageTests.cs 包含索引頁面模型的單元測試。
公用程式 TestDbContextOptions包含用來為每個 DAL 單元測試建立新資料庫內容選項的方法,以便將資料庫重設為每個測試的基準條件。

測試架構為 xUnit。 物件模擬架構為 Moq

資料存取層 (DAL 的單元測試)

訊息應用程式具有具有四個類別的 AppDbContext DAL, (src/RazorPagesTestSample/Data/AppDbContext.cs) 。 每個方法在測試應用程式中都有一個或兩個單元測試。

DAL 方法 函式
GetMessagesAsync List<Message>從資料庫取得,並依 Text 屬性排序。
AddMessageAsync Message將加入至資料庫。
DeleteAllMessagesAsync 從資料庫中刪除所有 Message 專案。
DeleteMessageAsync 從資料庫 Id 中刪除單一 Message

為每個測試建立新 AppDbContext 的時,DAL 需要 DbContextOptions 單元測試。 針對每個測試建立的 DbContextOptions 其中一種方法是使用 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.
}

類別 (UnitTests/DataAccessLayerTest.cs) 中 DataAccessLayerTest 的每個測試方法都遵循類似的排列-Act Assert 模式:

  1. 排列:資料庫設定為測試及/或定義預期的結果。
  2. Act:執行測試。
  3. 判斷提示:判斷測試結果是否成功的判斷提示。

例如, DeleteMessageAsync 方法會負責移除其 Id (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();
    }
}

這種方法有兩種測試。 當訊息存在於資料庫中時,一項測試會檢查此方法是否會刪除訊息。 另一種方法會測試如果刪除的訊息 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));
    }
}

首先,此方法會執行「排列」步驟,以進行「動作」步驟的準備。 系統會取得並保存植入 seedMessages 訊息。 植入訊息會儲存在資料庫中。 具有 Id 之的 1 訊息會設定為 [刪除]。 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 方法會傳遞 recId1 的:

// Act
await db.DeleteMessageAsync(recId);

最後,方法 Messages 會從內容中取得,並將它與 expectedMessages 判斷提示兩者相等的判斷提示比較:

// 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));

為了比較兩者 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));
    }
}

頁面模型方法的單元測試

另一組單元測試負責測試頁面模型方法。 在訊息應用程式中,索引頁面模型可在的 IndexModel 類別 src/RazorPagesTestSample/Pages/Index.cshtml.cs 中找到。

頁面模型方法 函式
OnGetAsync 使用 GetMessagesAsync 方法,從 DAL 取得適用于 UI 的訊息。
OnPostAddMessageAsync 如果 ModelState 有效,則呼叫 AddMessageAsync 會將訊息加入至資料庫。
OnPostDeleteAllMessagesAsync 呼叫 DeleteAllMessagesAsync 以刪除資料庫中的所有訊息。
OnPostDeleteMessageAsync 執行 DeleteMessageAsync 以刪除具有指定之的 Id 訊息。
OnPostAnalyzeMessagesAsync 如果資料庫中有一或多個訊息,則會計算每個訊息的平均單字數。

頁面模型方法會使用類別 (tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs) 中 IndexPageTests 的七個測試進行測試。 測試會使用熟悉的「排列-判斷提示-Act」模式。 這些測試著重于:

  • 判斷在 ModelState 無效時,方法是否遵循正確的行為。
  • 確認方法會產生正確 IActionResult 的。
  • 檢查是否已正確設定屬性值。

這組測試通常會模擬 DAL 的方法,為執行頁面模型方法的 Act 步驟產生預期的資料。 例如, GetMessagesAsync 的方法 AppDbContext 會模擬以產生輸出。 當頁面模型方法執行此方法時,mock 會傳回結果。 資料不是來自資料庫。 這會建立可預測、可靠的測試條件,以便在頁面模型測試中使用 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);

OnGetAsync當方法在 Act 步驟中執行時,它會呼叫頁面模型的 GetMessagesAsync 方法。

單元測試動作步驟 (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 屬性進行比較。 測試會判斷兩個 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));

此群組中的其他測試會建立頁面模型物件,這些物件包含 DefaultHttpContext 、、, ActionContext 以建立 PageContextViewDataDictionaryPageContextModelStateDictionary 。 這些都適用于進行測試。 例如,訊息應用程式會建立 ModelState 錯誤, AddModelError 以檢查執行時 OnPostAddMessageAsync 是否傳回有效 PageResult 的:

[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);
}

其他資源