自动化测试

介绍

ABP框架的设计考虑了可测试性. 有一些不同级别的自动化测试:

  • 单元测试: 通常只测试一个类(或者一起测试几个类). 这些测试会很快. 然而, 你通常需要处理对服务依赖项的模拟.
  • 集成测试: 你通常会测试一个服务, 但这一次你不会模拟基本的基础设施和服务, 以查看它们是否正确地协同工作.
  • 用户界面测试: 测试应用程序的UI, 就像用户与应用程序交互一样.

单元测试 vs 集成测试

与单元测试相比, 集成测试有一些显著的优势:

  • 编写更加简单 因为你不需要模拟和处理依赖关系.
  • 你的测试代码运行于所有真正的服务和基础设施(包括数据库映射和查询), 因此它更接近于真正的应用程序测试.

同时它们有一些缺点:

  • 与单元测试相比, 它们更慢, 因为所有的基础设施都准备好了测试用例.
  • 服务中的一个bug可能会导致多个测试用例失败, 因此在某些情况下, 可能会更难找到真正的问题.

我们建议混合使用: 在必要的地方编写单元测试或集成测试, 并且有效的编写和维护它.

应用程序启动模板

测试基础设施提供应用程序启动模板 , 并已经正确安装和配置.

测试项目

请参见Visual Studio中的以下解决方案:

solution-test-projects

按层级系统分为多个测试项目:

  • Domain.Tests 用于测试领域层对象 (例如领域服务实体).
  • Application.Tests 用于测试应用层对象 (例如应用服务).
  • EntityFrameworkCore.Tests 用于测试你的自定义仓储实现或EF Core映射(如果你使用其他数据访问)的话, 该项目将有所不同).
  • Web.Tests 用于测试UI层(如页面、控制器和视图组件). 该项目仅适用于MVC / Razor页面应用程序.
  • TestBase 包含一些由其他项目共享/使用的类.

HttpApi.Client.ConsoleTestApp 不是自动化测试的应用程序. 它是一个示例的控制台应用程序, 展示了如何从.NET控制台应用程序中调用HTTP API.

以下的部分将介绍这些项目中包含的基类和其他基础设施.

测试基础设施

解决方案中已经安装了以下库:

虽然你可以用自己喜欢的工具替换它们, 但本文档和示例将基于这些工具.

测试资源管理器

你可以在Visual Studio中使用测试资源管理器查看和运行测试. 其他IDE, 请参阅它们自己的文档.

打开测试资源管理器

打开测试菜单下的测试资源管理器(如果尚未打开):

vs-test-explorer

运行测试

然后, 你可以单击在视图中运行所有测试或运行按钮来运行测试. 初始启动模板为你提供了一些测试用例:

vs-startup-template-tests

并行运行测试

支持并行运行测试. 强烈建议并行运行所有测试, 这比逐个运行测试要快得多.

要启用它, 请单击设置(齿轮)按钮附近的插入符号图标, 然后选择并行运行测试.

vs-run-tests-in-parallel

单元测试

对于单元测试, 不需要太多的配置. 通常会实例化你的类, 并对要测试的对象提供一些预先配置的模拟对象.

没有依赖项的类

要测试的类没有依赖项是最简单的情况, 你可以直接实例化类, 调用其方法并做出断言.

示例: 测试实体

假设你有一个 Issue 实体, 如下所示:

  1. using System;
  2. using Volo.Abp.Domain.Entities;
  3. namespace MyProject.Issues
  4. {
  5. public class Issue : AggregateRoot<Guid>
  6. {
  7. public string Title { get; set; }
  8. public string Description { get; set; }
  9. public bool IsLocked { get; set; }
  10. public bool IsClosed { get; private set; }
  11. public DateTime? CloseDate { get; private set; }
  12. public void Close()
  13. {
  14. IsClosed = true;
  15. CloseDate = DateTime.UtcNow;
  16. }
  17. public void Open()
  18. {
  19. if (!IsClosed)
  20. {
  21. return;
  22. }
  23. if (IsLocked)
  24. {
  25. throw new IssueStateException("You can not open a locked issue!");
  26. }
  27. IsClosed = true;
  28. CloseDate = null;
  29. }
  30. }
  31. }

请注意, IsClosedCloseDate属性具有私有setter, 可以使用Open()Close()方法强制执行某些业务逻辑:

  • 无论何时关闭issue, CloseDate都应设置为当前时间.
  • 如果issue被锁定, 则无法重新打开. 如果它被重新打开, CloseDate应该设置为null.

由于Issue实体是领域层的一部分, 所以我们应该在Domain.Tests项目中测试它. 在Domain.Tests项目中创建一个Issue_Tests类:

  1. using Shouldly;
  2. using Xunit;
  3. namespace MyProject.Issues
  4. {
  5. public class Issue_Tests
  6. {
  7. [Fact]
  8. public void Should_Set_The_CloseDate_Whenever_Close_An_Issue()
  9. {
  10. // Arrange
  11. var issue = new Issue();
  12. issue.CloseDate.ShouldBeNull(); // null at the beginning
  13. // Act
  14. issue.Close();
  15. // Assert
  16. issue.IsClosed.ShouldBeTrue();
  17. issue.CloseDate.ShouldNotBeNull();
  18. }
  19. }
  20. }

这个测试遵循AAA(Arrange-Act-Assert)模式:

  • Arrange 部分创建一个Issue实体, 并确保CloseDate在初始值为null.
  • Act 部分执行我们想要测试的方法.
  • Assert 部分检查Issue属性是否与我们预期的相同.

[Fact]属性由xUnit并将方法标记为测试方法. Should...扩展方法由Shouldly提供. 你可以直接使用xUnit中的Assert类, 使用Shouldly让它更舒适、更直观.

当你执行测试时, 你将看到它成功通过:

issue-first-test

让我们再添加两种测试方法:

  1. [Fact]
  2. public void Should_Allow_To_ReOpen_An_Issue()
  3. {
  4. // Arrange
  5. var issue = new Issue();
  6. issue.Close();
  7. // Act
  8. issue.Open();
  9. // Assert
  10. issue.IsClosed.ShouldBeFalse();
  11. issue.CloseDate.ShouldBeNull();
  12. }
  13. [Fact]
  14. public void Should_Not_Allow_To_ReOpen_A_Locked_Issue()
  15. {
  16. // Arrange
  17. var issue = new Issue();
  18. issue.Close();
  19. issue.IsLocked = true;
  20. // Act & Assert
  21. Assert.Throws<IssueStateException>(() =>
  22. {
  23. issue.Open();
  24. });
  25. }

Assert.Throws 检查执行的代码是否匹配引发的异常.

有关这些库的更多信息, 请参阅xUnit & Shoudly的文档.

具有依赖项的类

如果你的服务中有依赖项, 并且你想对该服务进行单元测试, 那么你需要模拟这些依赖项.

示例: 测试领域服务

假设你有一个IssueManager 领域服务, 定义如下:

  1. using System;
  2. using System.Threading.Tasks;
  3. using Volo.Abp;
  4. using Volo.Abp.Domain.Services;
  5. namespace MyProject.Issues
  6. {
  7. public class IssueManager : DomainService
  8. {
  9. public const int MaxAllowedOpenIssueCountForAUser = 3;
  10. private readonly IIssueRepository _issueRepository;
  11. public IssueManager(IIssueRepository issueRepository)
  12. {
  13. _issueRepository = issueRepository;
  14. }
  15. public async Task AssignToUserAsync(Issue issue, Guid userId)
  16. {
  17. var issueCount = await _issueRepository.GetIssueCountOfUserAsync(userId);
  18. if (issueCount >= MaxAllowedOpenIssueCountForAUser)
  19. {
  20. throw new BusinessException(
  21. code: "IM:00392",
  22. message: $"You can not assign more" +
  23. $"than {MaxAllowedOpenIssueCountForAUser} issues to a user!"
  24. );
  25. }
  26. issue.AssignedUserId = userId;
  27. }
  28. }
  29. }

IssueManager依赖于IssueRepository服务, 在本例中将模拟该服务.

业务逻辑: 示例AssignToUserAsync不允许向用户分配超过3个issue (MaxAllowedOpenIssueCountForAUser常量). 在这种情况下, 如果要分配issue, 首先需要取消现有issue的分配.

下面的测试用例给出一个有效的赋值:

  1. using System;
  2. using System.Threading.Tasks;
  3. using NSubstitute;
  4. using Shouldly;
  5. using Volo.Abp;
  6. using Xunit;
  7. namespace MyProject.Issues
  8. {
  9. public class IssueManager_Tests
  10. {
  11. [Fact]
  12. public async Task Should_Assign_An_Issue_To_A_User()
  13. {
  14. // Arrange
  15. var userId = Guid.NewGuid();
  16. var fakeRepo = Substitute.For<IIssueRepository>();
  17. fakeRepo.GetIssueCountOfUserAsync(userId).Returns(1);
  18. var issueManager = new IssueManager(fakeRepo);
  19. var issue = new Issue();
  20. // Act
  21. await issueManager.AssignToUserAsync(issue, userId);
  22. //Assert
  23. issue.AssignedUserId.ShouldBe(userId);
  24. await fakeRepo.Received(1).GetIssueCountOfUserAsync(userId);
  25. }
  26. }
  27. }
  • Substitute.For<IIssueRepository> 创建一个模拟(假)对象, 该对象被传递到IssueManager构造函数中.
  • fakeRepo.GetIssueCountOfUserAsync(userId).Returns(1) 确保仓储中的GetIssueContofuseRasync方法返回1.
  • issueManager.AssignToUserAsync 不会引发任何异常, 因为仓储统计当前分配的issue数量并且返回1.
  • issue.AssignedUserId.ShouldBe(userId); 行检查AssignedUserId的值是否正确.
  • await fakeRepo.Received(1).GetIssueCountOfUserAsync(userId); 检查 IssueManager 实际只调用了 GetIssueCountOfUserAsync 方法一次.

让我们添加第二个测试, 看看它是否能阻止将issue分配给超过分配数量的用户:

  1. [Fact]
  2. public async Task Should_Not_Allow_To_Assign_Issues_Over_The_Limit()
  3. {
  4. // Arrange
  5. var userId = Guid.NewGuid();
  6. var fakeRepo = Substitute.For<IIssueRepository>();
  7. fakeRepo
  8. .GetIssueCountOfUserAsync(userId)
  9. .Returns(IssueManager.MaxAllowedOpenIssueCountForAUser);
  10. var issueManager = new IssueManager(fakeRepo);
  11. // Act & Assert
  12. var issue = new Issue();
  13. await Assert.ThrowsAsync<BusinessException>(async () =>
  14. {
  15. await issueManager.AssignToUserAsync(issue, userId);
  16. });
  17. issue.AssignedUserId.ShouldBeNull();
  18. await fakeRepo.Received(1).GetIssueCountOfUserAsync(userId);
  19. }

有关模拟的更多信息, 请参阅NSubstitute文档.

模拟单个依赖项相对容易. 但是, 当依赖关系增长时, 设置测试对象和模拟所有依赖关系变得越来越困难. 请参阅不需要模拟依赖项的Integration Tests部分.

提示: 共享测试类构造函数

xUnit 为每个测试方法创建一个新测试类实例(本例中为IssueManager_Tests). 因此, 你可以将一些Arrange代码移动到构造函数中, 以减少代码重复. 构造函数将针对每个测试用例执行, 并且不会相互影响, 即使它们是并行工作.

示例: 重构IssueManager_Tests以减少代码重复

  1. using System;
  2. using System.Threading.Tasks;
  3. using NSubstitute;
  4. using Shouldly;
  5. using Volo.Abp;
  6. using Xunit;
  7. namespace MyProject.Issues
  8. {
  9. public class IssueManager_Tests
  10. {
  11. private readonly Guid _userId;
  12. private readonly IIssueRepository _fakeRepo;
  13. private readonly IssueManager _issueManager;
  14. private readonly Issue _issue;
  15. public IssueManager_Tests()
  16. {
  17. _userId = Guid.NewGuid();
  18. _fakeRepo = Substitute.For<IIssueRepository>();
  19. _issueManager = new IssueManager(_fakeRepo);
  20. _issue = new Issue();
  21. }
  22. [Fact]
  23. public async Task Should_Assign_An_Issue_To_A_User()
  24. {
  25. // Arrange
  26. _fakeRepo.GetIssueCountOfUserAsync(_userId).Returns(1);
  27. // Act
  28. await _issueManager.AssignToUserAsync(_issue, _userId);
  29. //Assert
  30. _issue.AssignedUserId.ShouldBe(_userId);
  31. await _fakeRepo.Received(1).GetIssueCountOfUserAsync(_userId);
  32. }
  33. [Fact]
  34. public async Task Should_Not_Allow_To_Assign_Issues_Over_The_Limit()
  35. {
  36. // Arrange
  37. _fakeRepo
  38. .GetIssueCountOfUserAsync(_userId)
  39. .Returns(IssueManager.MaxAllowedOpenIssueCountForAUser);
  40. // Act & Assert
  41. await Assert.ThrowsAsync<BusinessException>(async () =>
  42. {
  43. await _issueManager.AssignToUserAsync(_issue, _userId);
  44. });
  45. _issue.AssignedUserId.ShouldBeNull();
  46. await _fakeRepo.Received(1).GetIssueCountOfUserAsync(_userId);
  47. }
  48. }
  49. }

保持测试代码整洁, 以创建可维护的测试组件.

集成测试

你还可以按照Web应用程序开发教程学习开发全栈应用程序, 包括集成测试.

集成测试基础

ABP为编写集成测试提供了完整的基础设施. 所有ABP基础设施和服务都将在你的测试中执行. 应用程序启动模板附带了为你预先配置的必要基础设施;

数据库

启动模板使用EF Core配置内存中的SQLite数据库(对于MongoDB, 它使用Mongo2Go). 因此, 所有配置和查询都是针对真实数据库执行的, 你甚至可以测试数据库事务.

使用内存中的SQLite数据库有两个主要优点:

  • 它比外部DBMS更快.
  • 它会为每个测试用例创建一个新的数据库, 这样测试就不会相互影响.

提示: 不要将EF Core的内存数据库用于高级集成测试. 它不是一个真正的DBMS, 在细节上有很多不同. 例如, 它不支持事务和回滚场景, 因此无法真正测试失败的场景. 另一方面, 内存中的SQLite是一个真正的DBMS, 支持SQL数据库的基本功能.

种子数据

针对空数据库编写测试是不现实的. 在大多数情况下, 需要在数据库中保存一些初始数据. 例如, 如果你编写了一个查询、更新和删除产品的测试类, 那么在执行测试用例之前, 在数据库中有一些产品数据会很有帮助.

ABP的种子数据系统是一种强大的初始化数据的方法. 应用程序启动模板在.TestBase项目中有一个YourProjectTestDataSeedContributor类. 你可以在其中添加, 以获得可用于每个测试方法的初始数据.

示例: 创建一些Issue作为种子数据

  1. using System.Threading.Tasks;
  2. using MyProject.Issues;
  3. using Volo.Abp.Data;
  4. using Volo.Abp.DependencyInjection;
  5. namespace MyProject
  6. {
  7. public class MyProjectTestDataSeedContributor
  8. : IDataSeedContributor, ITransientDependency
  9. {
  10. private readonly IIssueRepository _issueRepository;
  11. public MyProjectTestDataSeedContributor(IIssueRepository issueRepository)
  12. {
  13. _issueRepository = issueRepository;
  14. }
  15. public async Task SeedAsync(DataSeedContext context)
  16. {
  17. await _issueRepository.InsertAsync(
  18. new Issue
  19. {
  20. Title = "Test issue one",
  21. Description = "Test issue one description",
  22. AssignedUserId = TestData.User1Id
  23. });
  24. await _issueRepository.InsertAsync(
  25. new Issue
  26. {
  27. Title = "Test issue two",
  28. Description = "Test issue two description",
  29. AssignedUserId = TestData.User1Id
  30. });
  31. await _issueRepository.InsertAsync(
  32. new Issue
  33. {
  34. Title = "Test issue three",
  35. Description = "Test issue three description",
  36. AssignedUserId = TestData.User1Id
  37. });
  38. await _issueRepository.InsertAsync(
  39. new Issue
  40. {
  41. Title = "Test issue four",
  42. Description = "Test issue four description",
  43. AssignedUserId = TestData.User2Id
  44. });
  45. }
  46. }
  47. }

还创建了一个静态类来存储用户的 Id:

  1. using System;
  2. namespace MyProject
  3. {
  4. public static class TestData
  5. {
  6. public static Guid User1Id = Guid.Parse("41951813-5CF9-4204-8B18-CD765DBCBC9B");
  7. public static Guid User2Id = Guid.Parse("2DAB4460-C21B-4925-BF41-A52750A9B999");
  8. }
  9. }

通过这种方式, 我们可以使用这些已知Issue和用户的Id来运行测试.

示例: 测试领域服务

AbpIntegratedTest<T>类 (定义在Volo.Abp.TestBase) 用于编写集成到ABP框架的测试. T是用于设置和初始化应用程序的根模块的类型.

应用程序启动模板在每个测试项目中都有基类, 因此你可以从这些基类派生, 以使其更简单.

IssueManager测试将被重写成集成测试

  1. using System.Threading.Tasks;
  2. using Shouldly;
  3. using Volo.Abp;
  4. using Xunit;
  5. namespace MyProject.Issues
  6. {
  7. public class IssueManager_Integration_Tests : MyProjectDomainTestBase
  8. {
  9. private readonly IssueManager _issueManager;
  10. private readonly Issue _issue;
  11. public IssueManager_Integration_Tests()
  12. {
  13. _issueManager = GetRequiredService<IssueManager>();
  14. _issue = new Issue
  15. {
  16. Title = "Test title",
  17. Description = "Test description"
  18. };
  19. }
  20. [Fact]
  21. public async Task Should_Not_Allow_To_Assign_Issues_Over_The_Limit()
  22. {
  23. // Act & Assert
  24. await Assert.ThrowsAsync<BusinessException>(async () =>
  25. {
  26. await _issueManager.AssignToUserAsync(_issue, TestData.User1Id);
  27. });
  28. _issue.AssignedUserId.ShouldBeNull();
  29. }
  30. [Fact]
  31. public async Task Should_Assign_An_Issue_To_A_User()
  32. {
  33. // Act
  34. await _issueManager.AssignToUserAsync(_issue, TestData.User2Id);
  35. //Assert
  36. _issue.AssignedUserId.ShouldBe(TestData.User2Id);
  37. }
  38. }
  39. }
  • 第一个测试方法将issue分配给User1, 其中User1已经分配了种子数据代码中的3个issue. 因此, 它抛出了一个BusinessException.
  • 第二种测试方法将issue分配给User2, User2只分配了一个issue. 因此, 该方法成功了.

这个类通常位于.Domain.Tests项目中, 因为它测试位于.Domain项目中的类. 它派生自MyProjectDomainTestBase, 并已经为正确运行测试进行了配置.

编写这样一个集成测试类非常简单. 另一个好处是, 在以后向IssueManager类添加另一个依赖项时, 不需要更改测试类.

示例: 测试应用服务

测试应用服务并没有太大的不同. 假设你已经创建了一个IssueAppService, 定义如下:

  1. using System.Collections.Generic;
  2. using System.Threading.Tasks;
  3. using Volo.Abp.Application.Services;
  4. namespace MyProject.Issues
  5. {
  6. public class IssueAppService : ApplicationService, IIssueAppService
  7. {
  8. private readonly IIssueRepository _issueRepository;
  9. public IssueAppService(IIssueRepository issueRepository)
  10. {
  11. _issueRepository = issueRepository;
  12. }
  13. public async Task<List<IssueDto>> GetListAsync()
  14. {
  15. var issues = await _issueRepository.GetListAsync();
  16. return ObjectMapper.Map<List<Issue>, List<IssueDto>>(issues);
  17. }
  18. }
  19. }

(假设你还定义了IIssueAppServiceIssueDto, 并在IssueIssueDto之间创建了对象映射)

现在, 你可以在.Application.Tests项目中编写一个测试类:

  1. using System.Threading.Tasks;
  2. using Shouldly;
  3. using Xunit;
  4. namespace MyProject.Issues
  5. {
  6. public class IssueAppService_Tests : MyProjectApplicationTestBase
  7. {
  8. private readonly IIssueAppService _issueAppService;
  9. public IssueAppService_Tests()
  10. {
  11. _issueAppService = GetRequiredService<IIssueAppService>();
  12. }
  13. [Fact]
  14. public async Task Should_Get_All_Issues()
  15. {
  16. //Act
  17. var issueDtos = await _issueAppService.GetListAsync();
  18. //Assert
  19. issueDtos.Count.ShouldBeGreaterThan(0);
  20. }
  21. }
  22. }

就这么简单. 此测试方法测试的所有内容, 包括应用服务、EF Core映射、对象到对象映射和仓储实现. 通过这种方式, 你可以完全测试解决方案的应用层和领域层.

处理集成测试中的工作单元

ABP的工作单元系统控制应用程序中的数据库连接和事务管理. 它可以在你编写应用程序代码时无缝工作, 因此你可能没有意识到它.

在ABP框架中, 所有数据库操作都必须在一个工作单元作用域内执行. 当你测试应用服务方法时, 工作单元的作用域将是应用服务方法的作用域. 如果你正在测试仓储方法, 那么工作单元作用域将是你的仓储方法的作用域.

在某些情况下, 你可能需要手动控制工作单元作用域. 可以考虑下面的测试方法:

  1. public class IssueRepository_Tests : MyProjectDomainTestBase
  2. {
  3. private readonly IRepository<Issue, Guid> _issueRepository;
  4. public IssueRepository_Tests()
  5. {
  6. _issueRepository = GetRequiredService<IRepository<Issue, Guid>>();
  7. }
  8. public async Task Should_Query_By_Title()
  9. {
  10. IQueryable<Issue> queryable = await _issueRepository.GetQueryableAsync();
  11. var issue = queryable.FirstOrDefaultAsync(i => i.Title == "My issue title");
  12. issue.ShouldNotBeNull();
  13. }
  14. }

我们正在使用_issueRepository.GetQueryableAsync获取IQueryable<Issue> 对象. 然后, 我们使用FirstOrDefaultAsync方法按标题查询issue. 此时执行数据库查询, 你将会得到一个异常, 表明没有起作用的工作单元.

要使该测试正常工作, 你应该手动启动工作单元作用域, 如下所示:

  1. public class IssueRepository_Tests : MyProjectDomainTestBase
  2. {
  3. private readonly IRepository<Issue, Guid> _issueRepository;
  4. private readonly IUnitOfWorkManager _unitOfWorkManager;
  5. public IssueRepository_Tests()
  6. {
  7. _issueRepository = GetRequiredService<IRepository<Issue, Guid>>();
  8. _unitOfWorkManager = GetRequiredService<IUnitOfWorkManager>();
  9. }
  10. public async Task Should_Query_By_Title()
  11. {
  12. using (var uow = _unitOfWorkManager.Begin())
  13. {
  14. IQueryable<Issue> queryable = await _issueRepository.GetQueryableAsync();
  15. var issue = queryable.FirstOrDefaultAsync(i => i.Title == "My issue title");
  16. issue.ShouldNotBeNull();
  17. await uow.CompleteAsync();
  18. }
  19. }
  20. }

我们已经使用了IUnitOfWorkManager服务来创建一个工作单元作用域, 然后在该作用域内调用了FirstOrDefaultAsync方法, 所以不再有问题了.

请注意, 我们测试了FirstOrDefaultAsync来演示工作单元的问题. 作为一个好的标准, 编写自己的代码.

使用DbContext

在某些情况下, 你可能希望使用Entity Framework的DbContext对象来执行测试方法中的数据库操作. 在这种情况下, 可以使用IDbContextProvider<T>服务在工作单元内获取DbContext实例.

下面的示例展示了如何在测试方法中创建DbContext对象:

  1. public class MyDbContext_Tests : MyProjectDomainTestBase
  2. {
  3. private readonly IDbContextProvider<MyProjectDbContext> _dbContextProvider;
  4. private readonly IUnitOfWorkManager _unitOfWorkManager;
  5. public IssueRepository_Tests()
  6. {
  7. _dbContextProvider = GetRequiredService<IDbContextProvider<MyProjectDbContext>>();
  8. _unitOfWorkManager = GetRequiredService<IUnitOfWorkManager>();
  9. }
  10. public async Task Should_Query_By_Title()
  11. {
  12. using (var uow = _unitOfWorkManager.Begin())
  13. {
  14. var dbContext = await _dbContextProvider.GetDbContextAsync();
  15. var issue = await dbContext.Issues.FirstOrDefaultAsync(i => i.Title == "My issue title");
  16. issue.ShouldNotBeNull();
  17. await uow.CompleteAsync();
  18. }
  19. }
  20. }

就像我们在集成测试中处理工作单元一节中所做的那样, 我们应该在起作用的工作单元内执行DbContext操作.

对于MongoDB, 你可以使用IMongoDbContextProvider<T>服务获取DbContext对象, 并在测试方法中直接使用MongoDB APIs.

用户界面测试

一般来说, 有两种类型的UI测试:

非可视化测试

此类测试完全取决于UI框架的选择:

  • 对于MVC / Razor页面UI, 通常向服务器发出请求, 获取HTML, 并测试返回的结果中是否存在一些预期的DOM元素.
  • Angular有自己的基础设施和实践来测试组件、视图和服务.

请参阅以下文档以了解非可视化UI测试:

可视化测试

与真实用户一样, 可视化测试用于与应用程序UI交互. 它全面测试应用程序, 包括页面和组件的外观.

可视化UI测试超出了ABP框架的范围. 行业中有很多工具(比如Selenium)可以用来测试应用程序的UI.