简介

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 官方的默认模版也是这样。集成测试基类的核心代码很简单,就是在无参构造函数的内部进行初始化动作,且在过程中按顺序执行两个生命周期方法。

简易流程图:

start=>start: 开始单元测试 op1=>operation: BeforeAddApplication() op2=>operation: 注册模块服务 op3=>operation: AfterAddApplication() op4=>operation: 模块初始化 op5=>operation: 执行单元测试 end=>end: 销毁相关资源

start->op1->op2->op3->op4->op5->end

public abstract class AbpIntegratedTest<TStartupModule> : AbpTestBaseWithServiceProvider, IDisposablewhere 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 测试基类继承编写单元/集成测试,各个类型的关系如下。

AbpTestBaseWithServiceProvider#ServiceProvider#GetService() : <T>#GetRequiredService() : <T>AbpIntegratedTest<TStartupModule>#BeforeAddApplication(IServiceCollection service)#SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options)#AfterAddApplication(IServiceCollection services)#CreateServiceProvider(IServiceCollection service)+Dispose()AbpAspNetCoreIntegratedTestBase<TStartup>#TestServer Server#HttpClient Client-IHost host#CreateHostBuilder() : IHostBuilder#ConfigureServices(HostBuilderContext context, IServiceCollection services)#GetUrl_OfType_TController() : string#GetUrl_OfType_TController(string actionName) : string#GetUrl_OfType_TController(string actionName, object queryStringParamsAsAnonymousObject) : string+Dispose()IDispose<<interface>> IDispose

针对 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. 单元测试相关推荐

  1. [Abp vNext 源码分析] - 5. DDD 的领域层支持(仓储、实体、值对象)

    一.简要介绍 ABP vNext 框架本身就是围绕着 DDD 理念进行设计的,所以在 DDD 里面我们能够见到的实体.仓储.值对象.领域服务,ABP vNext 框架都为我们进行了实现,这些基础设施都 ...

  2. [Abp vNext 源码分析] - 4. 工作单元

    一.简要说明 统一工作单元是一个比较重要的基础设施组件,它负责管理整个业务流程当中涉及到的数据库事务,一旦某个环节出现异常自动进行回滚处理. 在 ABP vNext 框架当中,工作单元被独立出来作为一 ...

  3. [Abp vNext 源码分析] - 19. 多租户

    一.简介 ABP vNext 原生支持多租户体系,可以让开发人员快速地基于框架开发 SaaS 系统.ABP vNext 实现多租户的思路也非常简单,通过一个 TenantId 来分割各个租户的数据,并 ...

  4. [Abp vNext 源码分析] - 3. 依赖注入与拦截器

    一.简要说明 ABP vNext 框架在使用依赖注入服务的时候,是直接使用的微软提供的 Microsoft.Extensions.DependencyInjection 包.这里与原来的 ABP 框架 ...

  5. [Abp vNext 源码分析] - 2. 模块系统的变化

    一.简要说明 本篇文章主要分析 Abp vNext 当中的模块系统,从类型构造层面上来看,Abp vNext 当中不再只是单纯的通过 AbpModuleManager 来管理其他的模块,它现在则是 I ...

  6. [Abp vNext 源码分析] - 1. 框架启动流程分析

    一.简要说明 本篇文章主要剖析与讲解 Abp vNext 在 Web API 项目下的启动流程,让大家了解整个 Abp vNext 框架是如何运作的.总的来说 ,Abp vNext 比起 ABP 框架 ...

  7. apache dubbo 源码分析系列汇总

    Dubbo(读音[ˈdʌbəʊ])是阿里巴巴公司开源的一个高性能优秀的服务框架,使得应用可通过高性能的 RPC 实现服务的输出和输入功能,可以和 Spring框架无缝集成.后面捐献给了知名的开源社区 ...

  8. 【转】ABP源码分析二十八:ABP.MemoryDB

    这个模块简单,且无实际作用(该模块用于支持ABP框架单元测试的).一般实际项目中都有用数据库做持久化,用了数据库就无法用这个MemoryDB 模块了.原因在于ABP限制了UnitOfWork的类型只能 ...

  9. 【转】ABP源码分析二十五:EventBus

    IEventData/EventData: 封装了EventData信息,触发event的源对象和时间 IEventBus/EventBus: 定义和实现了了一系列注册,注销和触发事件处理函数的方法. ...

最新文章

  1. 重磅丨人工智能三年国家战略公布:8大领域要火!
  2. Ghost后不能启动解决小工具
  3. python select模块_Python之select模块解析
  4. 简单分析几个常见的排序算法(C语言)
  5. 微信小程序的提交审核流程
  6. 最常用计算机语音,计算机常用词汇--语言及服务器篇
  7. 部署Hadoop2.0高性能集群
  8. oracle查询当天数据三种方式性能对比
  9. WEB前端视频教程网站推荐
  10. 怎么学计算机制作ppt教程,ppt 制作教程步骤(新手电脑制作 ppt 详细步骤)
  11. Permute3 mac最新多种媒体视频格式转换工具
  12. 数据处理——偏最小二乘法
  13. win10局域网 开启网络发现,无法找到本机
  14. UCanCode发布跨平台开源组态\ 建模\仿真\工控VX++ 2021
  15. 【ES】Elasticsearch的特点优点 为什么比MySQL快?
  16. 面试B站,结果面试官牵着一条狗出来面试我....这是什么操作??
  17. 怪物猎人世界取得服务器信息,怪物猎人世界 Steam好友联机服务器选择工具V1.2...
  18. 时序预测 | MATLAB实现ARIMA时间序列预测(GDP预测)
  19. Android 无需 root 卸载系统应用
  20. NYOJ 304 节能(DP)

热门文章

  1. IfElseActivity
  2. java dateutil 获取时间戳_java DateUtil工具类时间戳类型转换详解
  3. vue xunidom_vue的虚拟dom(Virtual DOM )
  4. vue---day03
  5. 《智能家居》培训第六天------2019-01-10
  6. 2-1 gradle安装
  7. 关于input type=file 限制文件上传类型
  8. 《那些年啊,那些事——一个程序员的奋斗史》——126
  9. 忙碌的三月,小感慨下~
  10. vlc传输_如何使用VLC通过网络流式传输视频和音乐