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

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

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

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

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

示例项目包含两个应用:

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

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

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

测试应用组织

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

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

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

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

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

DAL 方法 Function
GetMessagesAsync 从按 List<Message> 属性排序的数据库获取 Text
AddMessageAsync 向数据库添加 Message
DeleteAllMessagesAsync 从数据库中删除所有 Message 条目。
DeleteMessageAsync Message 从数据库中删除单个 Id

为每个测试创建新的 DbContextOptions 时,DAL 的单元测试需要 AppDbContext。 为每个测试创建 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 方法并传入值为 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));
    }
}

页面模型方法的单元测试

另一组单元测试负责测试页面模型方法。 在消息应用中,可在 IndexModelsrc/RazorPagesTestSample/Pages/Index.cshtml.cs 类中找到索引页面模型。

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

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

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

这一组测试经常模拟 DAL 的方法,以生成执行页面模型方法的“执行”步骤的预期数据。 例如,模拟 GetMessagesAsyncAppDbContext 方法以生成输出。 当页面模型方法执行此方法时,模拟返回结果。 数据不来自数据库。 这会创建可预测的可靠测试条件,以便在页面模型测试中使用 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 步骤中,从页面模型的 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));

此组的其他测试创建页面模型对象,这些对象包括 DefaultHttpContextModelStateDictionary、用于建立 ActionContextPageContextViewDataDictionaryPageContext。 这些对象对于执行测试很有用。 例如,消息应用与 ModelState 建立 AddModelError 错误,以检查执行 PageResult 时是否返回有效的 OnPostAddMessageAsync

[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 应用或测试概念,请参阅以下主题:

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

示例项目包含两个应用:

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

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

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

测试应用组织

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

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

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

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

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

DAL 方法 Function
GetMessagesAsync 从按 List<Message> 属性排序的数据库获取 Text
AddMessageAsync 向数据库添加 Message
DeleteAllMessagesAsync 从数据库中删除所有 Message 条目。
DeleteMessageAsync Message 从数据库中删除单个 Id

为每个测试创建新的 DbContextOptions 时,DAL 的单元测试需要 AppDbContext。 为每个测试创建 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 方法并传入值为 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));
    }
}

页面模型方法的单元测试

另一组单元测试负责测试页面模型方法。 在消息应用中,可在 IndexModelsrc/RazorPagesTestSample/Pages/Index.cshtml.cs 类中找到索引页面模型。

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

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

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

这一组测试经常模拟 DAL 的方法,以生成执行页面模型方法的“执行”步骤的预期数据。 例如,模拟 GetMessagesAsyncAppDbContext 方法以生成输出。 当页面模型方法执行此方法时,模拟返回结果。 数据不来自数据库。 这会创建可预测的可靠测试条件,以便在页面模型测试中使用 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 步骤中,从页面模型的 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));

此组的其他测试创建页面模型对象,这些对象包括 DefaultHttpContextModelStateDictionary、用于建立 ActionContextPageContextViewDataDictionaryPageContext。 这些对象对于执行测试很有用。 例如,消息应用与 ModelState 建立 AddModelError 错误,以检查执行 PageResult 时是否返回有效的 OnPostAddMessageAsync

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

其他资源