ASP.NET Core 中的 Razor Pages 單元測試

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

  • 在應用程式建構期間,Razor 頁面應用程式的元件既能獨立運作,也可作為一個單元。
  • 類別和方法的責任範圍有限。
  • 另有其他文件說明應用程式應如何運行。
  • 回歸是由程式碼更新帶來的錯誤,會在自動化建置和部署過程中發現。

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

檢視或下載範例程式碼 \(英文\) (如何下載)

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

App 專案資料夾 描述
訊息應用程式 src/RazorPagesTestSample 允許使用者新增訊息、刪除一則訊息、刪除所有訊息,以及分析訊息 (查明每則訊息的平均字數)。
測試應用程式 tests/RazorPagesTestSample.Tests 用來單元測試訊息應用程式的 DAL 和索引頁面模型。

您可以使用 IDE 的內建測試功能,例如 Visual Studio,來執行測試。 如果使用 Visual Studio Code 或命令列,請在 tests/RazorPagesTestSample.Tests 資料夾中的命令提示字元執行下列命令:

dotnet test

訊息應用程式組織

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

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

†EF 主題 使用 InMemory 測試,說明如何使用記憶體內部資料庫搭配 MSTest 進行測試。 本主題使用 xUnit 測試架構。 不同測試架構的測試概念和測試實作彼此相似但並不相同。

雖然範例應用程式不使用存放庫模式,也不是工作單位 (UoW) 模式的有效範例,但 Razor Pages 支援此類開發模式。 如需詳細資訊,請參閱設計基礎結構持續性層測試 ASP.NET Core 中的控制器邏輯 (範例會實作存放庫模式)。

測試應用程式組織

測試應用程式是 test/RazorPagesTestSample.Tests 資料夾內的主控台應用程式。

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

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

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

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

DAL 方法 函式
GetMessagesAsync 從依 Text 屬性排序的資料庫取得 List<Message>
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.
}

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

  1. Arrange (安排):資料庫已針對測試進行設定,並/或已定義預期的結果。
  2. Act (作動):執行測試。
  3. Assert (斷定):做出判斷以確定測試結果是否成功。

例如,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));
    }
}

首先,該方法會執行 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 進行比較以斷定兩者相等:

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

頁面模型方法的單元測試

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

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

頁面模型方法會使用 IndexPageTests 類別 (tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs) 中的七個測試來進行測試。 這些測試會採取慣用的 Arrange-Assert-Act 模式。 這些測試著重於:

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

這個測試群組通常會模擬 DAL 的方法,以針對執行頁面模型方法時的 Act 步驟產生預期的資料。 例如,會模擬 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 步驟中,會從頁面模型的 Messages 屬性指派實際訊息 (actualMessages)。 指派訊息時也會執行類型檢查。 會依 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));

此群組中的其他測試會建立頁面模型物件,其中包含 DefaultHttpContextModelStateDictionary、用於建立 PageContextActionContextViewDataDictionaryPageContext。 這些在進行測試時很有用。 例如,訊息應用程式會使用 AddModelError 建立 ModelState 錯誤,以檢查執行 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 Pages 應用程式的單元測試。 資料存取層 (DAL) 和頁面模型的測試有助於確保:

  • 在應用程式建構期間,Razor 頁面應用程式的元件既能獨立運作,也可作為一個單元。
  • 類別和方法的責任範圍有限。
  • 另有其他文件說明應用程式應如何運行。
  • 回歸是由程式碼更新帶來的錯誤,會在自動化建置和部署過程中發現。

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

檢視或下載範例程式碼 \(英文\) (如何下載)

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

App 專案資料夾 描述
訊息應用程式 src/RazorPagesTestSample 允許使用者新增訊息、刪除一則訊息、刪除所有訊息,以及分析訊息 (查明每則訊息的平均字數)。
測試應用程式 tests/RazorPagesTestSample.Tests 用來單元測試訊息應用程式的 DAL 和索引頁面模型。

您可以使用 IDE 的內建測試功能,例如 Visual Studio,來執行測試。 如果使用 Visual Studio Code 或命令列,請在 tests/RazorPagesTestSample.Tests 資料夾中的命令提示字元執行下列命令:

dotnet test

訊息應用程式組織

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

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

†EF 主題 使用 InMemory 測試,說明如何使用記憶體內部資料庫搭配 MSTest 進行測試。 本主題使用 xUnit 測試架構。 不同測試架構的測試概念和測試實作彼此相似但並不相同。

雖然範例應用程式不使用存放庫模式,也不是工作單位 (UoW) 模式的有效範例,但 Razor Pages 支援此類開發模式。 如需詳細資訊,請參閱設計基礎結構持續性層測試 ASP.NET Core 中的控制器邏輯 (範例會實作存放庫模式)。

測試應用程式組織

測試應用程式是 test/RazorPagesTestSample.Tests 資料夾內的主控台應用程式。

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

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

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

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

DAL 方法 函式
GetMessagesAsync 從依 Text 屬性排序的資料庫取得 List<Message>
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.
}

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

  1. Arrange (安排):資料庫已針對測試進行設定,並/或已定義預期的結果。
  2. Act (作動):執行測試。
  3. Assert (斷定):做出判斷以確定測試結果是否成功。

例如,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));
    }
}

首先,該方法會執行 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 進行比較以斷定兩者相等:

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

頁面模型方法的單元測試

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

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

頁面模型方法會使用 IndexPageTests 類別 (tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs) 中的七個測試來進行測試。 這些測試會採取慣用的 Arrange-Assert-Act 模式。 這些測試著重於:

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

這個測試群組通常會模擬 DAL 的方法,以針對執行頁面模型方法時的 Act 步驟產生預期的資料。 例如,會模擬 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 步驟中,會從頁面模型的 Messages 屬性指派實際訊息 (actualMessages)。 指派訊息時也會執行類型檢查。 會依 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));

此群組中的其他測試會建立頁面模型物件,其中包含 DefaultHttpContextModelStateDictionary、用於建立 PageContextActionContextViewDataDictionaryPageContext。 這些在進行測試時很有用。 例如,訊息應用程式會使用 AddModelError 建立 ModelState 錯誤,以檢查執行 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);
}

其他資源