十一、Abp vNext 基础篇丨测试
前言
祝大家国庆快乐,本来想国庆之前更新完的,结果没写完,今天把剩下的代码补了一下总算ok了。
本章节也是我们后端日常开发中最重要的一步就是测试,我们经常听到的单元测试、集成测试、UI测试、系统测试,还有就是最常见的(人肉测试),这些理论知识我记得老张有个视频讲了2篇,欢迎大家可以去群里骚扰老张,我找不到那个链接了。
话不多说直接上车!
理论速过
ABP 框架的设计考虑了可测试性。有一些不同级别的自动化测试;
- 单元测试:您通常测试单个类(或几个类一起)。这些测试会很快。但是,您通常需要处理服务依赖项的模拟。
- 集成测试:您通常会测试服务,但这次您不会模拟基本的基础设施和服务,以查看它们是否可以正常协同工作。
- UI 测试:您测试应用程序的 UI,就像用户与您的应用程序交互一样。###
单元测试与集成测试
与单元测试相比,集成测试有一些显着的优势;
- 更容易编写,因为您不需要建立模拟和处理依赖项。
- 测试代码与所有真实的服务和基础设施(包括数据库映射和查询)一起运行,因此它更接近于真实的应用程序测试。
虽然它们有一些缺点;
- 与单元测试相比,它们更慢,因为所有的基础设施都是为每个测试用例准备的。
- 服务中的一个错误可能会导致多个测试用例被破坏,因此在某些情况下可能更难找到真正的问题。
我们建议混合使用:在必要的地方编写单元或集成测试,并且您认为编写和维护它是有效的。
Abp的默认已经帮我们生成了测试模板
- Domain.Tests用于测试您的域层对象(如域服务和实体)。
- Application.Tests用于测试您的应用程序层(如Application Services)。
- EntityFrameworkCore.Tests用于测试您的自定义存储库实现或 EF Core 映射(如果您使用其他数据库提供程序,此项目将有所不同)。
- TestBase 包含一些被其他项目共享/使用的类。
注意:HttpApi.Client.ConsoleTestApp不是自动化测试应用程序。这是一个示例控制台应用程序,展示了如何从 .NET 控制台应用程序使用您的 HTTP API。
单元测试
简单来看一下如何完成一个单元测试,依Blog
实体为例,实体是领域层的一部分,我们应该在Domain.Tests项目中对其进行测试。Blog_Tests在Domain.Tests项目中创建一个类(领域服务同理、)
此测试遵循 AAA(安排-行为-断言)模式;
- 安排部分创建一个Blog实体。
- 行为部分执行我们要为此案例测试的方法。
- 断言部分检查Blog属性是否与我们期望的相同。
public class Blog_Tests
{
[Theory]
[InlineData("aaa")]
[InlineData("bbb")]
public void SetName(string name)
{
var blog = new Blog(Guid.NewGuid(), "test blog", "test");
blog.SetName(name);
blog.Name.ShouldBe(name);
}
[Theory]
[InlineData("aaa")]
[InlineData("bbb")]
public void SetShortName(string name)
{
var blog = new Blog(Guid.NewGuid(), "test blog", "test");
blog.SetShortName(name);
blog.ShortName.ShouldBe(name);
}
}
集成测试
ABP 提供了一个完整的基础设施来编写集成测试。所有 ABP 基础设施和服务都将在您的测试中执行。应用程序启动模板附带为您预先配置的必要基础设施,启动模板配置为对 EF Core使用内存中的 SQLite数据库(对于 MongoDB,它使用Mongo2Go库)。因此,所有配置和查询都是针对真实数据库执行的,您甚至可以测试数据库事务。
种子数据
针对空数据库编写测试是不切实际的。大多数情况下,需要在数据库中获取一些初始数据。
在Bcvp.Blog.Core.TestBase
层的CoreTestDataSeedContributor.cs
中创建种子数据,这样我们就可以使用这些数据来测试了。
public class CoreTestData : ISingletonDependency
{
public Guid Blog1Id { get; } = Guid.NewGuid();
public Guid Blog1Post1Id { get; } = Guid.NewGuid();
public Guid Blog1Post2Id { get; } = Guid.NewGuid();
public Guid Blog1Post1Comment1Id { get; } = Guid.NewGuid();
public Guid Blog1Post1Comment2Id { get; } = Guid.NewGuid();
public string Tag1Name { get; } = "Tag1Name";
public string Tag2Name { get; } = "Tag2Name";
}
public class CoreTestDataSeedContributor : IDataSeedContributor, ITransientDependency
{
private readonly CoreTestData _testData;
private readonly IBlogRepository _blogRepository;
private readonly IPostRepository _postRepository;
private readonly ICommentRepository _commentRepository;
private readonly ITagRepository _tagRepository;
private readonly ICurrentTenant _currentTenant;
public CoreTestDataSeedContributor(
CoreTestData testData,
IBlogRepository blogRepository,
IPostRepository postRepository,
ICommentRepository commentRepository,
ITagRepository tagRepository,
ICurrentTenant currentTenant)
{
_testData = testData;
_blogRepository = blogRepository;
_postRepository = postRepository;
_commentRepository = commentRepository;
_tagRepository = tagRepository;
_currentTenant = currentTenant;
}
public async Task SeedAsync(DataSeedContext context)
{
/* Seed additional test data... */
using (_currentTenant.Change(context?.TenantId))
{
await SeedBlogsAsync();
await SeedPostsAsync();
await SeedCommentsAsync();
await SeedTagsAsync();
}
}
private async Task SeedBlogsAsync()
{
await _blogRepository.InsertAsync(new BlogCore.Blogs.Blog(_testData.Blog1Id, "The First Blog", "blog-1"));
}
private async Task SeedPostsAsync()
{
await _postRepository.InsertAsync(new Post(_testData.Blog1Post1Id, _testData.Blog1Id, "title", "coverImage", "url"));
await _postRepository.InsertAsync(new Post(_testData.Blog1Post2Id, _testData.Blog1Id, "title2", "coverImage2", "url2"));
}
public async Task SeedCommentsAsync()
{
await _commentRepository.InsertAsync(new Comment(_testData.Blog1Post1Comment1Id, _testData.Blog1Post1Id, null, "text"));
await _commentRepository.InsertAsync(new Comment(_testData.Blog1Post1Comment2Id, _testData.Blog1Post1Id, _testData.Blog1Post1Comment1Id, "text"));
}
public async Task SeedTagsAsync()
{
await _tagRepository.InsertAsync(new Tag(Guid.NewGuid(), _testData.Blog1Id, _testData.Tag1Name, 10));
await _tagRepository.InsertAsync(new Tag(Guid.NewGuid(), _testData.Blog1Id, _testData.Tag2Name));
}
}
小插曲
开头我说中间出了点问题,这里我说一下,首先是我有2个接口名字写错了,Resharper应该自动帮我加Async后缀,但是有2个接口没有很奇怪。
另一个问题之前我们开发的时候在用户操作上我们调用的UserLookupService<IdentityUsers>
,这个用法是错误的,UserLookupService
是基于IUser的一个扩展操作类public abstract class UserLookupService<TUser, TUserRepository>
,我们应该集成IUser然后继承它来写,代码我也补充上了,如果你是从第一章开始跟这个小插曲正好可以让你重新复习一遍开发流程也挺好的,但是如果不是单元测试我还真没发现这个坑(我也是第一次用UserLookupService 之前都是直接调用IdentityUserManager),所以单元测试还是非常重要的。
完结撒花
最后撒花,整个基础篇系列到此应该就结束了,从框架介绍到功能开发到测试,我们全方位对ABP进行了一次使用教学,虽然很多知识点没有用到,但是整篇文章操作下来,入门肯定是ok了。
后面我会开始对ABP独立的知识点进行讲解其中会涉及源码应用场景等多种案例,目前的安排是模块化->授权->eventbus->考虑中 如果你对那块知识点感兴趣可以联系我,大家一起边写边学。
联系作者:加群:867095512 @MrChuJiu
项目源码地址:https://github.com/BaseCoreVueProject/ABPvNext.Blog.Core