ASP.NET Core 中的 Razor Pages 单元测试

ASP.NET Core 支持 Razor Pages 应用的单元测试。 数据访问层 (DAL) 和页面模型测试有助于确保:

  • Razor Pages 应用的各个部分在应用构造过程中既可以独立运行,也可以作为一个整体运行。
  • 类和方法具有有限责任范围。
  • 存在有关应用应如何运行的其他文档。
  • 回归指代码更新引起的错误,可在自动生成和部署过程中出现。

本主题假定你对 Razor Pages 应用和单元测试有基本的了解。 如果你不熟悉 Razor Pages 应用或测试概念,请参阅以下主题:

查看或下载示例代码如何下载

示例项目包含两个应用:

应用 项目文件夹 描述
消息应用 src/RazorPagesTestSample 允许用户添加消息、删除一条消息、删除所有消息以及分析消息(查找每条消息的平均字词数)。
测试应用 tests/RazorPagesTestSample.Tests 用于对消息应用的 DAL 和索引页面模型进行单元测试。

可使用 IDE 的内置测试功能(例如 Visual StudioVisual Studio for Mac)运行测试。 如果使用 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 个字符。
  • 消息使用实体框架的内存中数据库†存储。
  • 应用在其数据库上下文类 AppDbContext (Data/AppDbContext.cs) 中包含 DAL。 DAL 方法标记为 virtual,这允许模拟在测试中使用的方法。
  • 如果应用启动时数据库为空,则消息存储初始化为三条消息。 这些 种子消息 也用于测试。

†EF 主题使用 InMemory 进行测试说明如何将内存中数据库用于 MSTest 测试。 本主题使用 xUnit 测试框架。 不同测试框架中的测试概念和测试实现相似,但不完全相同。

尽管示例应用未使用存储库模式且不是工作单元 (UoW) 模式的有效示例,但 Razor Pages 支持这些开发模式。 有关详细信息,请参阅设计基础设施持久性层ASP.NET Core 中的测试控制器逻辑(该示例实现存储库模式)。

测试应用组织

测试应用是 tests/RazorPagesTestSample.Tests 文件夹中的控制台应用。

测试应用文件夹 描述
UnitTests
  • DataAccessLayerTest.cs 包含 DAL 的单元测试。
  • IndexPageTests.cs 包含索引页面模型的单元测试。
实用工具 包含 TestDbContextOptions 方法,该方法用于为每个 DAL 单元测试创建新的数据库上下文选项,以便为每个测试将数据库重置为其基线条件。

测试框架为 xUnit。 对象模拟框架为 Moq

数据访问层 (DAL) 的单元测试

消息应用具有 DAL,其中 AppDbContext 类 (src/RazorPagesTestSample/Data/AppDbContext.cs) 中包含 4 个方法。 每个方法在测试应用中都有一到两个单元测试。

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) 中的每个测试方法都遵循类似的安排-执行-断言模式:

  1. 安排:为测试配置数据库和/或定义预期结果。
  2. 执行:执行测试。
  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 中。 种子消息会保存到数据库中。 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.cs 的 IndexModel 类中找到索引页面模型。

页面模型方法 函数
OnGetAsync 使用 GetMessagesAsync 方法从 DAL 获取 UI 的消息。
OnPostAddMessageAsync 如果 ModelState 有效,则调用 AddMessageAsync 将消息添加到数据库。
OnPostDeleteAllMessagesAsync 调用 DeleteAllMessagesAsync 以删除数据库中的所有消息。
OnPostDeleteMessageAsync 执行 DeleteMessageAsync 以删除指定了 Id 的消息。
OnPostAnalyzeMessagesAsync 如果数据库中有一条或多条消息,请计算每条消息的平均字词数。

使用 IndexPageTests 类 (tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs) 中的 7 个测试来测试页面模型方法。 测试使用熟悉的安排-断言-执行模式。 这些测试的重点在于:

  • 确定 ModelState 无效时方法是否遵循正确的行为模式。
  • 确认方法是否会生成正确的 IActionResult
  • 检查属性值分配是否正确进行。

这一组测试经常模拟 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);

在“执行”步骤中执行 OnGetAsync 方法时,它会调用页面模型的 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 步骤中,从页面模型的 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 Pages 应用的各个部分在应用构造过程中既可以独立运行,也可以作为一个整体运行。
  • 类和方法具有有限责任范围。
  • 存在有关应用应如何运行的其他文档。
  • 回归指代码更新引起的错误,可在自动生成和部署过程中出现。

本主题假定你对 Razor Pages 应用和单元测试有基本的了解。 如果你不熟悉 Razor Pages 应用或测试概念,请参阅以下主题:

查看或下载示例代码如何下载

示例项目包含两个应用:

应用 项目文件夹 描述
消息应用 src/RazorPagesTestSample 允许用户添加消息、删除一条消息、删除所有消息以及分析消息(查找每条消息的平均字词数)。
测试应用 tests/RazorPagesTestSample.Tests 用于对消息应用的 DAL 和索引页面模型进行单元测试。

可使用 IDE 的内置测试功能(例如 Visual StudioVisual Studio for Mac)运行测试。 如果使用 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 个字符。
  • 消息使用实体框架的内存中数据库†存储。
  • 应用在其数据库上下文类 AppDbContext (Data/AppDbContext.cs) 中包含 DAL。 DAL 方法标记为 virtual,这允许模拟在测试中使用的方法。
  • 如果应用启动时数据库为空,则消息存储初始化为三条消息。 这些 种子消息 也用于测试。

†EF 主题使用 InMemory 进行测试说明如何将内存中数据库用于 MSTest 测试。 本主题使用 xUnit 测试框架。 不同测试框架中的测试概念和测试实现相似,但不完全相同。

尽管示例应用未使用存储库模式且不是工作单元 (UoW) 模式的有效示例,但 Razor Pages 支持这些开发模式。 有关详细信息,请参阅设计基础设施持久性层ASP.NET Core 中的测试控制器逻辑(该示例实现存储库模式)。

测试应用组织

测试应用是 tests/RazorPagesTestSample.Tests 文件夹中的控制台应用。

测试应用文件夹 描述
UnitTests
  • DataAccessLayerTest.cs 包含 DAL 的单元测试。
  • IndexPageTests.cs 包含索引页面模型的单元测试。
实用工具 包含 TestDbContextOptions 方法,该方法用于为每个 DAL 单元测试创建新的数据库上下文选项,以便为每个测试将数据库重置为其基线条件。

测试框架为 xUnit。 对象模拟框架为 Moq

数据访问层 (DAL) 的单元测试

消息应用具有 DAL,其中 AppDbContext 类 (src/RazorPagesTestSample/Data/AppDbContext.cs) 中包含 4 个方法。 每个方法在测试应用中都有一到两个单元测试。

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) 中的每个测试方法都遵循类似的安排-执行-断言模式:

  1. 安排:为测试配置数据库和/或定义预期结果。
  2. 执行:执行测试。
  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 中。 种子消息会保存到数据库中。 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.cs 的 IndexModel 类中找到索引页面模型。

页面模型方法 函数
OnGetAsync 使用 GetMessagesAsync 方法从 DAL 获取 UI 的消息。
OnPostAddMessageAsync 如果 ModelState 有效,则调用 AddMessageAsync 将消息添加到数据库。
OnPostDeleteAllMessagesAsync 调用 DeleteAllMessagesAsync 以删除数据库中的所有消息。
OnPostDeleteMessageAsync 执行 DeleteMessageAsync 以删除指定了 Id 的消息。
OnPostAnalyzeMessagesAsync 如果数据库中有一条或多条消息,请计算每条消息的平均字词数。

使用 IndexPageTests 类 (tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs) 中的 7 个测试来测试页面模型方法。 测试使用熟悉的安排-断言-执行模式。 这些测试的重点在于:

  • 确定 ModelState 无效时方法是否遵循正确的行为模式。
  • 确认方法是否会生成正确的 IActionResult
  • 检查属性值分配是否正确进行。

这一组测试经常模拟 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);

在“执行”步骤中执行 OnGetAsync 方法时,它会调用页面模型的 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 步骤中,从页面模型的 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);
}

其他资源