集成测试
你也可以跟随 Web 应用开发教程 学习开发全栈应用,包括集成测试。
集成测试基础设施
ABP 提供了一套完整的基础设施来编写集成测试。所有 ABP 基础设施和服务都会在你的测试中正常运行。应用程序启动模板已经为你预先配置好了必要的基础设施。
数据库
启动模板配置为 EF Core 使用 内存中的 SQLite 数据库(对于 MongoDB,则使用 EphemeralMongo 库)。因此,所有的配置和查询都是针对真实的数据库执行的,你甚至可以测试数据库事务。
使用内存中的 SQLite 数据库有两个主要优点:
- 相比于外部数据库管理系统,速度更快。
- 它为每个测试用例创建 一个全新的数据库,因此测试之间不会相互影响。
提示:对于高级集成测试,不要使用 EF Core 的内存数据库。它不是一个真正的数据库管理系统,在细节上有很多差异。例如,它不支持事务和回滚场景,因此你无法真正测试失败的情况。而内存中的 SQLite 是一个真正的数据库管理系统,支持基本的 SQL 数据库功能。
种子数据
针对空数据库编写测试并不实用。在大多数情况下,你需要在数据库中有一些初始数据。例如,如果你编写一个测试类来查询、更新和删除产品,那么在执行测试用例之前,数据库中已经有几个产品将会很有帮助。
ABP 的 数据种子系统 是一种强大的初始化数据的方法。应用程序启动模板在 .TestBase 项目中有一个 YourProjectTestDataSeedContributor 类。你可以填充它以拥有每个测试方法都可以使用的初始数据。
示例:创建一些问题作为种子数据
using System.Threading.Tasks;
using MyProject.Issues;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
namespace MyProject
{
public class MyProjectTestDataSeedContributor
: IDataSeedContributor, ITransientDependency
{
private readonly IIssueRepository _issueRepository;
public MyProjectTestDataSeedContributor(IIssueRepository issueRepository)
{
_issueRepository = issueRepository;
}
public async Task SeedAsync(DataSeedContext context)
{
await _issueRepository.InsertAsync(
new Issue
{
Title = "测试问题一",
Description = "测试问题一描述",
AssignedUserId = TestData.User1Id
});
await _issueRepository.InsertAsync(
new Issue
{
Title = "测试问题二",
Description = "测试问题二描述",
AssignedUserId = TestData.User1Id
});
await _issueRepository.InsertAsync(
new Issue
{
Title = "测试问题三",
Description = "测试问题三描述",
AssignedUserId = TestData.User1Id
});
await _issueRepository.InsertAsync(
new Issue
{
Title = "测试问题四",
Description = "测试问题四描述",
AssignedUserId = TestData.User2Id
});
}
}
}
同时创建一个静态类来存储用户的 Id:
using System;
namespace MyProject
{
public static class TestData
{
public static Guid User1Id = Guid.Parse("41951813-5CF9-4204-8B18-CD765DBCBC9B");
public static Guid User2Id = Guid.Parse("2DAB4460-C21B-4925-BF41-A52750A9B999");
}
}
通过这种方式,我们可以使用这些已知的 Issues 和用户的 Id 来执行测试。
示例:测试领域服务
AbpIntegratedTest<T> 类(定义在 Volo.Abp.TestBase 包中)用于编写与 ABP 集成的测试。T 是用于设置和初始化应用程序的根模块类型。
应用程序启动模板 在每个测试项目中都有基类,因此你可以从这些基类派生,以简化操作。
请看重写为集成测试的 IssueManager 测试:
using System.Threading.Tasks;
using Shouldly;
using Volo.Abp;
using Xunit;
namespace MyProject.Issues
{
public abstract class IssueManager_Integration_Tests<TStartupModule> : MyProjectDomainTestBase<TStartupModule>
where TStartupModule : IAbpModule
{
private readonly IssueManager _issueManager;
private readonly Issue _issue;
protected IssueManager_Integration_Tests()
{
_issueManager = GetRequiredService<IssueManager>();
_issue = new Issue
{
Title = "测试标题",
Description = "测试描述"
};
}
[Fact]
public async Task Should_Not_Allow_To_Assign_Issues_Over_The_Limit()
{
// 操作 & 断言
await Assert.ThrowsAsync<BusinessException>(async () =>
{
await _issueManager.AssignToUserAsync(_issue, TestData.User1Id);
});
_issue.AssignedUserId.ShouldBeNull();
}
[Fact]
public async Task Should_Assign_An_Issue_To_A_User()
{
// 操作
await _issueManager.AssignToUserAsync(_issue, TestData.User2Id);
// 断言
_issue.AssignedUserId.ShouldBe(TestData.User2Id);
}
}
}
IssueManager_Integration_Tests类是一个抽象类,该类中的测试在测试资源管理器中不可见,请参阅下面的 在 EF Core 和 MongoDB 中实现单元测试 部分,了解如何在测试资源管理器中列出并运行它们。
- 第一个测试方法将问题分配给用户 1,而用户 1 在数据种子代码中已经分配了 3 个问题。因此,它会抛出一个
BusinessException。 - 第二个测试方法将问题分配给用户 2,用户 2 只分配了 1 个问题。因此,该方法成功。
这个类通常位于 .Domain.Tests 项目中,因为它测试的是 .Domain 项目中的一个类。它派生自 MyProjectDomainTestBase,后者已经配置好,可以正确运行测试。
编写这样的集成测试类非常简单。另一个好处是,当你向 IssueManager 类添加另一个依赖项时,以后不需要更改测试类。
示例:测试应用服务
测试 应用服务 也没有太大不同。假设你创建了一个如下定义的 IssueAppService:
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
namespace MyProject.Issues
{
public class IssueAppService : ApplicationService, IIssueAppService
{
private readonly IIssueRepository _issueRepository;
public IssueAppService(IIssueRepository issueRepository)
{
_issueRepository = issueRepository;
}
public async Task<List<IssueDto>> GetListAsync()
{
var issues = await _issueRepository.GetListAsync();
return ObjectMapper.Map<List<Issue>, List<IssueDto>>(issues);
}
}
}
(假设你还定义了
IIssueAppService和IssueDto,并在Issue和IssueDto之间创建了 对象映射)
现在,你可以在 .Application.Tests 项目中编写一个测试类:
using System.Threading.Tasks;
using Shouldly;
using Xunit;
namespace MyProject.Issues
{
public abstract class IssueAppService_Tests<TStartupModule> : MyProjectApplicationTestBase<TStartupModule>
where TStartupModule : IAbpModule
{
private readonly IIssueAppService _issueAppService;
protected IssueAppService_Tests()
{
_issueAppService = GetRequiredService<IIssueAppService>();
}
[Fact]
public async Task Should_Get_All_Issues()
{
// 操作
var issueDtos = await _issueAppService.GetListAsync();
// 断言
issueDtos.Count.ShouldBeGreaterThan(0);
}
}
}
IssueAppService_Tests类是一个抽象类,该类中的测试在测试资源管理器中不可见,请参阅下面的 在 EF Core 和 MongoDB 中实现单元测试 部分,了解如何在测试资源管理器中列出并运行它们。
就是这么简单。这个测试方法测试了所有内容,包括应用服务、EF Core 映射、对象到对象映射以及仓储实现。通过这种方式,你可以完整地测试解决方案的应用层和领域层。
处理集成测试中的工作单元
ABP 的 工作单元 系统在你的应用程序中控制数据库连接和事务管理。它在编写应用程序代码时无缝工作,因此你可能没有意识到它的存在。
在 ABP 中,所有数据库操作都必须在工作单元范围内执行。当你测试一个 应用服务 方法时,工作单元的范围就是你的应用服务方法的范围。如果你测试一个 仓储 方法,工作单元的范围就是你的仓储方法的范围。
在某些情况下,你可能需要手动控制工作单元范围。考虑以下测试方法:
public abstract class IssueRepository_Tests<TStartupModule> : MyProjectDomainTestBase<TStartupModule>
where TStartupModule : IAbpModule
{
private readonly IRepository<Issue, Guid> _issueRepository;
protected IssueRepository_Tests()
{
_issueRepository = GetRequiredService<IRepository<Issue, Guid>>();
}
public async Task Should_Query_By_Title()
{
IQueryable<Issue> queryable = await _issueRepository.GetQueryableAsync();
var issue = queryable.FirstOrDefaultAsync(i => i.Title == "我的问题标题");
issue.ShouldNotBeNull();
}
}
IssueRepository_Tests类是一个抽象类,该类中的测试在测试资源管理器中不可见,请参阅下面的 在 EF Core 和 MongoDB 中实现单元测试 部分,了解如何在测试资源管理器中列出并运行它们。
我们使用 _issueRepository.GetQueryableAsync 来获取一个 IQueryable<Issue> 对象。然后,我们使用 FirstOrDefaultAsync 方法通过标题查询一个问题。此时会执行数据库查询,你会得到一个异常,提示没有活动的工作单元。
为了使该测试正常工作,你应该手动启动一个工作单元范围,如下例所示:
public abstract class IssueRepository_Tests<TStartupModule> : MyProjectDomainTestBase<TStartupModule>
where TStartupModule : IAbpModule
{
private readonly IRepository<Issue, Guid> _issueRepository;
private readonly IUnitOfWorkManager _unitOfWorkManager;
protected IssueRepository_Tests()
{
_issueRepository = GetRequiredService<IRepository<Issue, Guid>>();
_unitOfWorkManager = GetRequiredService<IUnitOfWorkManager>();
}
public async Task Should_Query_By_Title()
{
using (var uow = _unitOfWorkManager.Begin())
{
IQueryable<Issue> queryable = await _issueRepository.GetQueryableAsync();
var issue = queryable.FirstOrDefaultAsync(i => i.Title == "我的问题标题");
issue.ShouldNotBeNull();
await uow.CompleteAsync();
}
}
}
我们使用了 IUnitOfWorkManager 服务来创建一个工作单元范围,然后在该范围内调用 FirstOrDefaultAsync 方法,这样就不会再有问题了。
注意,我们测试了
FirstOrDefaultAsync来演示工作单元问题。通常,作为一个好的原则,你应该只为你自己的代码编写测试。
你也可以使用 WithUnitOfWorkAsync 来实现相同的功能,而不是在你的测试中编写相同的 using 块。
以下是使用 WithUnitOfWorkAsync 的相同测试场景:
public abstract class IssueRepository_Tests<TStartupModule> : MyProjectDomainTestBase<TStartupModule>
where TStartupModule : IAbpModule
{
private readonly IRepository<Issue, Guid> _issueRepository;
private readonly IUnitOfWorkManager _unitOfWorkManager;
protected IssueRepository_Tests()
{
_issueRepository = GetRequiredService<IRepository<Issue, Guid>>();
_unitOfWorkManager = GetRequiredService<IUnitOfWorkManager>();
}
public async Task Should_Query_By_Title()
{
await WithUnitOfWorkAsync(() =>
{
IQueryable<Issue> queryable = await _issueRepository.GetQueryableAsync();
var issue = queryable.FirstOrDefaultAsync(i => i.Title == "我的问题标题");
issue.ShouldNotBeNull();
});
}
}
WithUnitOfWorkAsync 方法有多个重载,你可以根据具体需求使用。
使用 DbContext
在某些情况下,你可能希望在测试方法中直接使用 Entity Framework 的 DbContext 对象 来执行数据库操作。在这种情况下,你可以在一个工作单元内使用 IDbContextProvider<T> 服务来获取一个 DbContext 实例。
以下示例展示了如何在测试方法中创建一个 DbContext 对象:
public abstract class MyDbContext_Tests<TStartupModule> : MyProjectDomainTestBase<TStartupModule>
where TStartupModule : IAbpModule
{
private readonly IDbContextProvider<MyProjectDbContext> _dbContextProvider;
private readonly IUnitOfWorkManager _unitOfWorkManager;
protected IssueRepository_Tests()
{
_dbContextProvider = GetRequiredService<IDbContextProvider<MyProjectDbContext>>();
_unitOfWorkManager = GetRequiredService<IUnitOfWorkManager>();
}
public async Task Should_Query_By_Title()
{
using (var uow = _unitOfWorkManager.Begin())
{
var dbContext = await _dbContextProvider.GetDbContextAsync();
var issue = await dbContext.Issues.FirstOrDefaultAsync(i => i.Title == "我的问题标题");
issue.ShouldNotBeNull();
await uow.CompleteAsync();
}
}
}
MyDbContext_Tests类是一个抽象类,该类中的测试在测试资源管理器中不可见,请参阅下面的 在 EF Core 和 MongoDB 中实现单元测试 部分,了解如何在测试资源管理器中列出并运行它们。
就像我们在 处理集成测试中的工作单元 部分所做的那样,我们应该在活动的工作单元内执行 DbContext 操作。
对于 MongoDB,你可以使用 IMongoDbContextProvider<T> 服务来获取一个 DbContext 对象,并在测试方法中直接使用 MongoDB API。
在 EF Core 和 MongoDB 中实现单元测试
.Domain.Test 和 .Application.Tests 项目中的单元测试类都是抽象类。因此,我们需要在 EF Core 或 MongoDB 测试项目中实现测试类才能运行测试,否则它们将被忽略。
下面是在 .EntityFrameworkCore.Tests 项目中实现 IssueManager_Integration_Tests 类的一个示例:
namespace MyProject.EntityFrameworkCore.Applications;
public class EfCoreIssueAppService_Tests : IssueAppService_Tests<MyProjectEntityFrameworkCoreTestModule>
{
}
通过从相关的抽象类派生,现在我们可以在测试资源管理器中看到所有测试并运行它们。
从文件夹结构中可以看出,所有测试都清晰地放置在相关的子文件夹中,并且通过这种分离,它们将在测试资源管理器中可见。因此,你可以清楚地看到哪些测试与哪些层和项目相关。
抠丁客



