[Abp vNext 源码分析] - 18. 单元测试
简介
ABP vNext 框架使用 xUnit 作为单元测试组件,官方的所有模块都编写了大量的 单元/集成测试 确保功能正常。由于 ABP vNext 模块化系统的原因,开发人员在建立单元测试项目的时候需要集成 Volo.Abp.UnitTest 项目,这样在执行单元测试的时候才不会缺少必要组件。
分析
ABP vNext 单元测试相关的类型最核心的是集成测试基类 AbpIntegratedTest
和 MVC 专用测试基类 AbpAspNetCoreIntegratedTestBase
,这两个基类核心工作就是初始化 IoC 容器并且初始化整个模块系统,只不过后者对 控制器 相关的组件进行了初始化配置,让开发人员可以针对 控制器 进行单元/集成测试。
从上图可以看到两个基类都继承自 AbpTestBaseWithServiceProvider
基类,在这个基类里面将 IServiceProvider
作为一个抽象成员。这是因为 MVC 和测试基类的 ServiceProvider
来源不一样,一个是 ABP vNext 根据 Application
类已注册 IoC 容器构建的,另一个使用的是 IHost
测试主机内的 ServiceProvider
。
单元测试执行本质上就是将测试类进行实例化,然后调用对应的单元测试方法,所以测试基类的初始化动作都是放在对应的无参构造函数。
虽然 Volo.Abp.UnitTest 也是单独的一个项目,它的 AbpTestBaseModule
是没有任何动作,仅仅是为了同其他项目保持一致,内部是没有任何代码。
using Volo.Abp.Modularity;
namespace Volo.Abp
{
public class AbpTestBaseModule : AbpModule
{
}
}
集成测试基类
一般来说,我们会直接从 AbpIntegratedTest
继承并实现我们需要的单元测试基类,包括 ABP vNext 官方的默认模版也是这样。集成测试基类的核心代码很简单,就是在无参构造函数的内部进行初始化动作,且在过程中按顺序执行两个生命周期方法。
简易流程图:
public abstract class AbpIntegratedTest<TStartupModule> : AbpTestBaseWithServiceProvider, IDisposable
where TStartupModule : IAbpModule
{
protected IAbpApplication Application { get; }
protected override IServiceProvider ServiceProvider => Application.ServiceProvider;
protected IServiceProvider RootServiceProvider { get; }
protected IServiceScope TestServiceScope { get; }
protected AbpIntegratedTest()
{
var services = CreateServiceCollection();
BeforeAddApplication(services);
var application = services.AddApplication<TStartupModule>(SetAbpApplicationCreationOptions);
Application = application;
AfterAddApplication(services);
// 根据已有 IServiceCollection 创建 IoC 容器。
RootServiceProvider = CreateServiceProvider(services);
TestServiceScope = RootServiceProvider.CreateScope();
// 使用子容器对 ABP 模块系统进行初始化。
application.Initialize(TestServiceScope.ServiceProvider);
}
// ... 其他代码。
}
上述代码可以看到默认的测试基类并没有直接使用 RootServiceProvider
,而是创建了一个子容器给 ABP vNext 使用,这主要是为了后续可以对容器进行销毁操作,具体可一看下面的 Dispose()
方法。
public virtual void Dispose()
{
Application.Shutdown();
TestServiceScope.Dispose();
Application.Dispose();
}
总的来说,测试基类就是构建了一个 IAbpApplication
对象,根据传入的 TStartupModule
模块进入拓扑排序过程,依次执行各个模块的生命周期配置。
MVC 测试基类
针对我们的 Http Api 层,如果需要对 Controller
进行测试的话,就需要从 MVC 测试基类继承编写单元/集成测试,各个类型的关系如下。
针对 AspNetCore 来说,ABP 创建了一个新的 Host 主机,在每次执行测试的时候会启动一个新的 Web 服务器。(并不会创建真实服务,不存在端口占用问题)
在基类当中,ABP 定义了两个属性 Server
和 Client
,它们都是 Mock 了对应的接口,方便后续的单元测试,这里的 ITestServerAccessor
接口是用于 Mock AspNetCoreTestDynamicProxyHttpClientFactory
接口所需要的。
AspNetCoreTestDynamicProxyHttpClientFactory
接口是 ABP 底层进行动态代理所使用的,在请求远程服务的时候会调用这个接口创建 HttpClient 对象。
protected AbpAspNetCoreIntegratedTestBase()
{
var builder = CreateHostBuilder();
_host = builder.Build();
_host.Start();
Server = _host.GetTestServer();
Client = _host.GetTestClient();
ServiceProvider = Server.Services;
ServiceProvider.GetRequiredService<ITestServerAccessor>().Server = Server;
}
从 UML 类图当中,可以看到基类定义了几个 GetUrl()
方法,这几个方法是根据 Controller 获取对应的请求路径。
这里我以一个 SampleController
控制器为例,它提供了一个 Index 方法,返回了一个页面内容。针对它来说,我们编写集成测试是这样操作的。
public class SimpleController : AbpController
{
public ActionResult Index()
{
return Content("Index-Result");
}
}
集成测试
public class SimpleController_Tests : AbpAspNetCoreIntegratedTestBase<Startup>
{
protected virtual async Task<HttpResponseMessage> GetResponseAsync(string url, HttpStatusCode expectedStatusCode = HttpStatusCode.OK)
{
using (var requestMessage = new HttpRequestMessage(HttpMethod.Get, url))
{
requestMessage.Headers.Add("Accept-Language", CultureInfo.CurrentUICulture.Name);
var response = await Client.SendAsync(requestMessage);
response.StatusCode.ShouldBe(expectedStatusCode);
return response;
}
}
protected virtual async Task<string> GetResponseAsStringAsync(string url, HttpStatusCode expectedStatusCode = HttpStatusCode.OK)
{
using (var response = await GetResponseAsync(url, expectedStatusCode))
{
return await response.Content.ReadAsStringAsync();
}
}
[Fact]
public async Task ActionResult_ContentResult()
{
var result = await GetResponseAsStringAsync(
GetUrl<SimpleController>(nameof(SimpleController.Index))
);
result.ShouldBe("Index-Result");
}
}
EF Core 的集成
在执行单元测试过程中,我们难免会对数据库进行操作。这个时候不可能连接真实数据库,就需要我们在测试基类当中进行一些初始化动作,将底层的数据库链接改为 SQLite 的内存模式。
public class SampleEntityFrameworkCoreTestModule : AbpModule
{
private SqliteConnection _sqliteConnection;
public override void ConfigureServices(ServiceConfigurationContext context)
{
ConfigureInMemorySqlite(context.Services);
}
private void ConfigureInMemorySqlite(IServiceCollection services)
{
// 建立链接并执行迁移。
_sqliteConnection = CreateDatabaseAndGetConnection();
// 使用 SQLite 作为 EF Provider。
services.Configure<AbpDbContextOptions>(options =>
{
options.Configure(context =>
{
context.DbContextOptions.UseSqlite(_sqliteConnection);
});
});
}
public override void OnApplicationShutdown(ApplicationShutdownContext context)
{
_sqliteConnection.Dispose();
}
private static SqliteConnection CreateDatabaseAndGetConnection()
{
// 使用 SQLite 的内存模式链接字符串。
var connection = new SqliteConnection("Data Source=:memory:");
connection.Open();
var options = new DbContextOptionsBuilder<SampleMigrationsDbContext>()
.UseSqlite(connection)
.Options;
// 执行迁移,构建表结构。
using (var context = new SampleMigrationsDbContext(options))
{
context.GetService<IRelationalDatabaseCreator>().CreateTables();
}
return connection;
}
}
总结
ABP 的测试更偏向于集成测试,因为各个功能都依赖于模块,所以在执行单元测试的时候会运行更长的时间。日常开发过程当中,我们更多地还是针对应用层进行测试就可以了,粒度更细的话也可以针对仓储层、领域层、API 层 编写测试即可。
为了保证项目质量,在开发完成之后编写单元/集成测试是每个开发人员应做的工作。编写单元/集成测试,虽然不能 100% 避免 BUG,但可以保证每次进行业务修改之后接口的正确性。
[Abp vNext 源码分析] - 18. 单元测试的更多相关文章
- [Abp vNext 源码分析] - 文章目录
一.简要介绍 ABP vNext 是 ABP 框架作者所发起的新项目,截止目前 (2019 年 2 月 18 日) 已经拥有 1400 多个 Star,最新版本号为 v 0.16.0 ,但还属于预览版 ...
- [Abp vNext 源码分析] - 5. DDD 的领域层支持(仓储、实体、值对象)
一.简要介绍 ABP vNext 框架本身就是围绕着 DDD 理念进行设计的,所以在 DDD 里面我们能够见到的实体.仓储.值对象.领域服务,ABP vNext 框架都为我们进行了实现,这些基础设施都 ...
- [Abp vNext 源码分析] - 4. 工作单元
一.简要说明 统一工作单元是一个比较重要的基础设施组件,它负责管理整个业务流程当中涉及到的数据库事务,一旦某个环节出现异常自动进行回滚处理. 在 ABP vNext 框架当中,工作单元被独立出来作为一 ...
- [Abp vNext 源码分析] - 11. 用户的自定义参数与配置
一.简要说明 文章信息: 基于的 ABP vNext 版本:1.0.0 创作日期:2019 年 10 月 23 日晚 更新日期:暂无 ABP vNext 针对用户可编辑的配置,提供了单独的 Volo. ...
- [Abp vNext 源码分析] - 13. 本地事件总线与分布式事件总线 (Rabbit MQ)
一.简要介绍 ABP vNext 封装了两种事件总线结构,第一种是 ABP vNext 自己实现的本地事件总线,这种事件总线无法跨项目发布和订阅.第二种则是分布式事件总线,ABP vNext 自己封装 ...
- [Abp vNext 源码分析] - 3. 依赖注入与拦截器
一.简要说明 ABP vNext 框架在使用依赖注入服务的时候,是直接使用的微软提供的 Microsoft.Extensions.DependencyInjection 包.这里与原来的 ABP 框架 ...
- [Abp vNext 源码分析] - 2. 模块系统的变化
一.简要说明 本篇文章主要分析 Abp vNext 当中的模块系统,从类型构造层面上来看,Abp vNext 当中不再只是单纯的通过 AbpModuleManager 来管理其他的模块,它现在则是 I ...
- [Abp vNext 源码分析] - 1. 框架启动流程分析
一.简要说明 本篇文章主要剖析与讲解 Abp vNext 在 Web API 项目下的启动流程,让大家了解整个 Abp vNext 框架是如何运作的.总的来说 ,Abp vNext 比起 ABP 框架 ...
- [Abp vNext 源码分析] - 6. DDD 的应用层支持 (应用服务)
一.简要介绍 ABP vNext 针对于应用服务层,为我们单独设计了一个模块进行实现,即 Volo.Abp.Ddd.Application 模块. PS:最近博主也是在恶补 DDD 相关的知识,这里推 ...
随机推荐
- vue学习遇到的问题
1.vue脚手架的安装,解决链接:https://www.cnblogs.com/qcq0703/p/14439467.html2.2.2.0+ 的版本里,当在组件上使用 v-for 时,key 现在 ...
- RocketMQ基础概念剖析,并分析一下Producer的底层源码
由于篇幅原因,本次的源码分析只限于Producer侧的发送消息的核心逻辑,我会通过流程图.代码注释.文字讲解的方式来对源码进行解释,后续应该会专门开几篇文章来做源码分析. 这篇博客聊聊关于Rocket ...
- SpringCloud(三):SpringCloud快速开发入门
3-1. 搭建和配置一个服务提供者 我们知道,SpringCloud 构建微服务是基于 SpringBoot 开发的.(如果SpringBoot不会的可以先看SpringBoot专栏) 1. 创建一 ...
- SpringCloud(一):微服务架构概述
1-1. 系统进化理论概述 在系统架构与设计的实践中,经历了两个阶段,一个阶段是早些年常见的集中式系统,一个阶段是近年来流行的分布式系统: 集中式系统: 集中式系统也叫单体应用,就是把所有的程序.功 ...
- 后端程序员之路 39、一个Protocol Buffer实例
实际工作的Protocol Buffer使用经验 # 写proto文件- 协议版本 项目用的是protobuf2,所以要指定 syntax = "proto2";- 包名 pack ...
- c++ vector对象
下面随笔讲解c++ vector对象. vector对象 为什么需要vector? 封装任何类型的动态数组,自动创建和删除. 数组下标越界检查. 封装的如ArrayOfPoints也提供了类似功能,但 ...
- uwsgi+nginx+virtualenv+supervisor
一.linux安装Python 1.依赖 `yum install gcc patch libffi-devel python-devel zlib-devel bzip2-devel openssl ...
- ajax轮询原理及其实现方式
ajax轮询原理及其实现方式 ajax轮询的两种方式 方式1:设定一个定时器,无论有无结果返回,时间一到就会继续发起请求,这种轮询耗费资源,也不一定能得到想要的数据,这样的轮询是不推荐的. 方式2: ...
- div+伪元素实现太极图
需求:使用div和伪元素实现阴阳太极图 图例: 代码: <html> <head> <title>太极图</title> <style type= ...
- 内置了一个缓冲区(数组)缓冲流BufferInputStream为何要配合字节数组的使用?
内置了一个缓冲区(数组)缓冲流BufferInputStream为何要配合字节数组的使用? 只为效率而积累[积少成多送一趟比送多趟快] 举例子:超市买30个鸡蛋回家煮 (1)读一个送一个(效率太低啦) ...