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

本文内容

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 文件夹中的命令提示符处执行以下命令:

  1. dotnet test

消息应用组织Message app organization

消息应用是具有以下特征的 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 中的测试控制器逻辑(该示例实现存储库模式)。

测试应用组织Test app organization

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

测试应用文件夹描述
UnitTests

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

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

数据访问层 (DAL) 的单元测试Unit tests of the data access layer (DAL)

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

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

为每个测试创建新的 AppDbContext 时,DAL 的单元测试需要 DbContextOptions为每个测试创建 DbContextOptions 的一个方法是使用 DbContextOptionsBuilder

  1. var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>()
  2. .UseInMemoryDatabase("InMemoryDb");
  3. using (var db = new AppDbContext(optionsBuilder.Options))
  4. {
  5. // Use the db here in the unit test.
  6. }

此方法的问题在于,每个测试收到的数据库都处于之前测试中的状态。尝试编写不会相互干扰的原子单元测试时,这可能会导致问题。若要强制 AppDbContext 为每个测试使用新的数据库上下文,请提供基于新服务提供程序的 DbContextOptions 实例。测试应用演示如何使用其 Utilities 类方法 TestDbContextOptions (tests/RazorPagesTestSample.Tests/Utilities/Utilities.cs) 执行此操作:

  1. public static DbContextOptions<AppDbContext> TestDbContextOptions()
  2. {
  3. // Create a new service provider to create a new in-memory database.
  4. var serviceProvider = new ServiceCollection()
  5. .AddEntityFrameworkInMemoryDatabase()
  6. .BuildServiceProvider();
  7. // Create a new options instance using an in-memory database and
  8. // IServiceProvider that the context should resolve all of its
  9. // services from.
  10. var builder = new DbContextOptionsBuilder<AppDbContext>()
  11. .UseInMemoryDatabase("InMemoryDb")
  12. .UseInternalServiceProvider(serviceProvider);
  13. return builder.Options;
  14. }

在 DAL 单元测试中使用 DbContextOptions 可使每个测试使用新的数据库实例自动运行:

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

DataAccessLayerTest 类 (UnitTests/DataAccessLayerTest.cs) 中的每个测试方法都遵循类似的安排-执行-断言模式:

  • 安排:为测试配置数据库和/或定义预期结果。
  • 执行:执行测试。
  • 断言:进行断言以确定测试结果是否成功。
    例如,DeleteMessageAsync 方法负责删除由其 Id (src/RazorPagesTestSample/Data/AppDbContext.cs) 标识的单个消息:
  1. public async virtual Task DeleteMessageAsync(int id)
  2. {
  3. var message = await Messages.FindAsync(id);
  4. if (message != null)
  5. {
  6. Messages.Remove(message);
  7. await SaveChangesAsync();
  8. }
  9. }

此方法有两个测试。一个测试检查当数据库中存在消息时该方法是否删除消息。另一个方法测试在要删除的消息 Id 不存在的情况下,数据库是否保持不变。DeleteMessageAsync_MessageIsDeleted_WhenMessageIsFound 方法如下所示:

  1. [Fact]
  2. public async Task DeleteMessageAsync_MessageIsDeleted_WhenMessageIsFound()
  3. {
  4. using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
  5. {
  6. // Arrange
  7. var seedMessages = AppDbContext.GetSeedingMessages();
  8. await db.AddRangeAsync(seedMessages);
  9. await db.SaveChangesAsync();
  10. var recId = 1;
  11. var expectedMessages =
  12. seedMessages.Where(message => message.Id != recId).ToList();
  13. // Act
  14. await db.DeleteMessageAsync(recId);
  15. // Assert
  16. var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
  17. Assert.Equal(
  18. expectedMessages.OrderBy(m => m.Id).Select(m => m.Text),
  19. actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
  20. }
  21. }

首先,方法执行“安排”步骤,并在该步骤中为“执行”步骤做好准备。获取种子消息并将其保存在 seedMessages 中。种子消息会保存到数据库中。Id1 的消息设置为删除。执行 DeleteMessageAsync 方法时,预期的消息应是除 Id1 的消息以外的所有消息。expectedMessages 变量表示此预期结果。

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

该方法执行:执行 DeleteMessageAsync 方法并传入值为 1recId

  1. // Act
  2. await db.DeleteMessageAsync(recId);

最后,该方法从上下文中获取 Messages 并将其与断言两者相等的 expectedMessages 进行比较:

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

若要比较两个 List<Message> 是否相同,请执行以下操作:

  • Id 排序消息。
  • Text 属性上比较消息对。

类似的测试方法 DeleteMessageAsync_NoMessageIsDeleted_WhenMessageIsNotFound 检查尝试删除不存在的消息的结果。在这种情况下,执行 DeleteMessageAsync 方法后,数据库中的预期消息数应等于实际消息数。数据库的内容不应有任何变化:

  1. [Fact]
  2. public async Task DeleteMessageAsync_NoMessageIsDeleted_WhenMessageIsNotFound()
  3. {
  4. using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
  5. {
  6. // Arrange
  7. var expectedMessages = AppDbContext.GetSeedingMessages();
  8. await db.AddRangeAsync(expectedMessages);
  9. await db.SaveChangesAsync();
  10. var recId = 4;
  11. // Act
  12. try
  13. {
  14. await db.DeleteMessageAsync(recId);
  15. }
  16. catch
  17. {
  18. // recId doesn't exist
  19. }
  20. // Assert
  21. var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
  22. Assert.Equal(
  23. expectedMessages.OrderBy(m => m.Id).Select(m => m.Text),
  24. actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
  25. }
  26. }

页面模型方法的单元测试Unit tests of the page model methods

另一组单元测试负责测试页面模型方法。在消息应用中,可在 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) 中的七个测试来测试页面模型方法。测试使用熟悉的安排-断言-执行模式。这些测试的重点在于:

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

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

OnGetAsync_PopulatesThePageModel_WithAListOfMessages 测试演示如何为页面模型模拟 GetMessagesAsync 方法:

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

在“执行”步骤中执行 OnGetAsync 方法时,它会调用页面模型的 GetMessagesAsync 方法。

单元测试“执行”步骤 (tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs):

  1. // Act
  2. await pageModel.OnGetAsync();

IndexPage 页面模型的 OnGetAsync 方法 (src/RazorPagesTestSample/Pages/Index.cshtml.cs):

  1. public async Task OnGetAsync()
  2. {
  3. Messages = await _db.GetMessagesAsync();
  4. }

DAL 中的 GetMessagesAsync 方法不会返回此方法调用的结果。方法的模拟版本返回结果。

Assert 步骤中,从页面模型的 Messages 属性分配实际消息 (actualMessages)。分配消息后,还会执行类型检查。预期消息和实际消息通过其 Text 属性进行比较。该测试断言两个 List<Message> 实例包含相同的消息。

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

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

  1. [Fact]
  2. public async Task OnPostAddMessageAsync_ReturnsAPageResult_WhenModelStateIsInvalid()
  3. {
  4. // Arrange
  5. var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>()
  6. .UseInMemoryDatabase("InMemoryDb");
  7. var mockAppDbContext = new Mock<AppDbContext>(optionsBuilder.Options);
  8. var expectedMessages = AppDbContext.GetSeedingMessages();
  9. mockAppDbContext.Setup(db => db.GetMessagesAsync()).Returns(Task.FromResult(expectedMessages));
  10. var httpContext = new DefaultHttpContext();
  11. var modelState = new ModelStateDictionary();
  12. var actionContext = new ActionContext(httpContext, new RouteData(), new PageActionDescriptor(), modelState);
  13. var modelMetadataProvider = new EmptyModelMetadataProvider();
  14. var viewData = new ViewDataDictionary(modelMetadataProvider, modelState);
  15. var tempData = new TempDataDictionary(httpContext, Mock.Of<ITempDataProvider>());
  16. var pageContext = new PageContext(actionContext)
  17. {
  18. ViewData = viewData
  19. };
  20. var pageModel = new IndexModel(mockAppDbContext.Object)
  21. {
  22. PageContext = pageContext,
  23. TempData = tempData,
  24. Url = new UrlHelper(actionContext)
  25. };
  26. pageModel.ModelState.AddModelError("Message.Text", "The Text field is required.");
  27. // Act
  28. var result = await pageModel.OnPostAddMessageAsync();
  29. // Assert
  30. Assert.IsType<PageResult>(result);
  31. }

其他资源Additional resources

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 文件夹中的命令提示符处执行以下命令:

  1. dotnet test

消息应用组织Message app organization

消息应用是具有以下特征的 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 中的测试控制器逻辑(该示例实现存储库模式)。

测试应用组织Test app organization

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

测试应用文件夹描述
UnitTests

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

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

数据访问层 (DAL) 的单元测试Unit tests of the data access layer (DAL)

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

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

为每个测试创建新的 AppDbContext 时,DAL 的单元测试需要 DbContextOptions为每个测试创建 DbContextOptions 的一个方法是使用 DbContextOptionsBuilder

  1. var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>()
  2. .UseInMemoryDatabase("InMemoryDb");
  3. using (var db = new AppDbContext(optionsBuilder.Options))
  4. {
  5. // Use the db here in the unit test.
  6. }

此方法的问题在于,每个测试收到的数据库都处于之前测试中的状态。尝试编写不会相互干扰的原子单元测试时,这可能会导致问题。若要强制 AppDbContext 为每个测试使用新的数据库上下文,请提供基于新服务提供程序的 DbContextOptions 实例。测试应用演示如何使用其 Utilities 类方法 TestDbContextOptions (tests/RazorPagesTestSample.Tests/Utilities/Utilities.cs) 执行此操作:

  1. public static DbContextOptions<AppDbContext> TestDbContextOptions()
  2. {
  3. // Create a new service provider to create a new in-memory database.
  4. var serviceProvider = new ServiceCollection()
  5. .AddEntityFrameworkInMemoryDatabase()
  6. .BuildServiceProvider();
  7. // Create a new options instance using an in-memory database and
  8. // IServiceProvider that the context should resolve all of its
  9. // services from.
  10. var builder = new DbContextOptionsBuilder<AppDbContext>()
  11. .UseInMemoryDatabase("InMemoryDb")
  12. .UseInternalServiceProvider(serviceProvider);
  13. return builder.Options;
  14. }

在 DAL 单元测试中使用 DbContextOptions 可使每个测试使用新的数据库实例自动运行:

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

DataAccessLayerTest 类 (UnitTests/DataAccessLayerTest.cs) 中的每个测试方法都遵循类似的安排-执行-断言模式:

  • 安排:为测试配置数据库和/或定义预期结果。
  • 执行:执行测试。
  • 断言:进行断言以确定测试结果是否成功。
    例如,DeleteMessageAsync 方法负责删除由其 Id (src/RazorPagesTestSample/Data/AppDbContext.cs) 标识的单个消息:
  1. public async virtual Task DeleteMessageAsync(int id)
  2. {
  3. var message = await Messages.FindAsync(id);
  4. if (message != null)
  5. {
  6. Messages.Remove(message);
  7. await SaveChangesAsync();
  8. }
  9. }

此方法有两个测试。一个测试检查当数据库中存在消息时该方法是否删除消息。另一个方法测试在要删除的消息 Id 不存在的情况下,数据库是否保持不变。DeleteMessageAsync_MessageIsDeleted_WhenMessageIsFound 方法如下所示:

  1. [Fact]
  2. public async Task DeleteMessageAsync_MessageIsDeleted_WhenMessageIsFound()
  3. {
  4. using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
  5. {
  6. // Arrange
  7. var seedMessages = AppDbContext.GetSeedingMessages();
  8. await db.AddRangeAsync(seedMessages);
  9. await db.SaveChangesAsync();
  10. var recId = 1;
  11. var expectedMessages =
  12. seedMessages.Where(message => message.Id != recId).ToList();
  13. // Act
  14. await db.DeleteMessageAsync(recId);
  15. // Assert
  16. var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
  17. Assert.Equal(
  18. expectedMessages.OrderBy(m => m.Id).Select(m => m.Text),
  19. actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
  20. }
  21. }

首先,方法执行“安排”步骤,并在该步骤中为“执行”步骤做好准备。获取种子消息并将其保存在 seedMessages 中。种子消息会保存到数据库中。Id1 的消息设置为删除。执行 DeleteMessageAsync 方法时,预期的消息应是除 Id1 的消息以外的所有消息。expectedMessages 变量表示此预期结果。

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

该方法执行:执行 DeleteMessageAsync 方法并传入值为 1recId

  1. // Act
  2. await db.DeleteMessageAsync(recId);

最后,该方法从上下文中获取 Messages 并将其与断言两者相等的 expectedMessages 进行比较:

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

若要比较两个 List<Message> 是否相同,请执行以下操作:

  • Id 排序消息。
  • Text 属性上比较消息对。

类似的测试方法 DeleteMessageAsync_NoMessageIsDeleted_WhenMessageIsNotFound 检查尝试删除不存在的消息的结果。在这种情况下,执行 DeleteMessageAsync 方法后,数据库中的预期消息数应等于实际消息数。数据库的内容不应有任何变化:

  1. [Fact]
  2. public async Task DeleteMessageAsync_NoMessageIsDeleted_WhenMessageIsNotFound()
  3. {
  4. using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
  5. {
  6. // Arrange
  7. var expectedMessages = AppDbContext.GetSeedingMessages();
  8. await db.AddRangeAsync(expectedMessages);
  9. await db.SaveChangesAsync();
  10. var recId = 4;
  11. // Act
  12. await db.DeleteMessageAsync(recId);
  13. // Assert
  14. var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
  15. Assert.Equal(
  16. expectedMessages.OrderBy(m => m.Id).Select(m => m.Text),
  17. actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
  18. }
  19. }

页面模型方法的单元测试Unit tests of the page model methods

另一组单元测试负责测试页面模型方法。在消息应用中,可在 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) 中的七个测试来测试页面模型方法。测试使用熟悉的安排-断言-执行模式。这些测试的重点在于:

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

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

OnGetAsync_PopulatesThePageModel_WithAListOfMessages 测试演示如何为页面模型模拟 GetMessagesAsync 方法:

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

在“执行”步骤中执行 OnGetAsync 方法时,它会调用页面模型的 GetMessagesAsync 方法。

单元测试“执行”步骤 (tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs):

  1. // Act
  2. await pageModel.OnGetAsync();

IndexPage 页面模型的 OnGetAsync 方法 (src/RazorPagesTestSample/Pages/Index.cshtml.cs):

  1. public async Task OnGetAsync()
  2. {
  3. Messages = await _db.GetMessagesAsync();
  4. }

DAL 中的 GetMessagesAsync 方法不会返回此方法调用的结果。方法的模拟版本返回结果。

Assert 步骤中,从页面模型的 Messages 属性分配实际消息 (actualMessages)。分配消息后,还会执行类型检查。预期消息和实际消息通过其 Text 属性进行比较。该测试断言两个 List<Message> 实例包含相同的消息。

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

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

  1. [Fact]
  2. public async Task OnPostAddMessageAsync_ReturnsAPageResult_WhenModelStateIsInvalid()
  3. {
  4. // Arrange
  5. var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>()
  6. .UseInMemoryDatabase("InMemoryDb");
  7. var mockAppDbContext = new Mock<AppDbContext>(optionsBuilder.Options);
  8. var expectedMessages = AppDbContext.GetSeedingMessages();
  9. mockAppDbContext.Setup(db => db.GetMessagesAsync()).Returns(Task.FromResult(expectedMessages));
  10. var httpContext = new DefaultHttpContext();
  11. var modelState = new ModelStateDictionary();
  12. var actionContext = new ActionContext(httpContext, new RouteData(), new PageActionDescriptor(), modelState);
  13. var modelMetadataProvider = new EmptyModelMetadataProvider();
  14. var viewData = new ViewDataDictionary(modelMetadataProvider, modelState);
  15. var tempData = new TempDataDictionary(httpContext, Mock.Of<ITempDataProvider>());
  16. var pageContext = new PageContext(actionContext)
  17. {
  18. ViewData = viewData
  19. };
  20. var pageModel = new IndexModel(mockAppDbContext.Object)
  21. {
  22. PageContext = pageContext,
  23. TempData = tempData,
  24. Url = new UrlHelper(actionContext)
  25. };
  26. pageModel.ModelState.AddModelError("Message.Text", "The Text field is required.");
  27. // Act
  28. var result = await pageModel.OnPostAddMessageAsync();
  29. // Assert
  30. Assert.IsType<PageResult>(result);
  31. }

其他资源Additional resources