ABP入门系列(11)——编写单元测试

1. 前言

In computer programming, unit testing is a software testing method by which individual units of source code, sets of one or more computer program modules together with associated control data, usage procedures, and operating procedures, are tested to determine whether they are fit for use.
在电脑编程中,单元测试是一种软件测试方法。通过该方法来测试代码的单个单元、一个或多个计算机程序模块的集合以及相关联的控制数据、使用过程和操作过程,以确定它们是否适合使用。

单元测试是保证软件质量的重要指标。单元测试能够帮助我们提高程序的稳定性,使用单元测试更容易发现问题,也便于重构。TDD(测试驱动开发)的原理就是在开发功能代码之前先编写单元测试。但写单元测试也是一个浩大的工程。其中优劣也只有真正实践才能有更深的体会。

TDD开发流程

Abp作为一个优秀的框架,自然也应用了单元测试。Abp的代码都通过XUnit进行了单元测试。下面我们就延续Abp的优良作风,为我们的业务代码编写单元测试。

2. 对Abp模板测试项目一探究竟

Test Project

2.1. 测试项目结构

如图所示,通过在Abp官网创建的模板项目中,默认就已经为我们创建好了测试项目。并对Session、User创建了单元测试。其中LearningMpaAbpTestBase是继承的集成测试基类,主要用来伪造一个数据库连接。该项目添加了对Application、Core、EntityFramework项目的引用,以便于我们针对它们进行测试,从这我们也可以看出,Abp是按照Service-->Repository-->Domain这条线来进行集成测试
打开测试项目的NuGet程序包我们可以发现主要依赖了以下几个NuGet包:

  • Abp.TestBase:提供了测试基类和基础架构以便我们创建单元集成测试。
  • Effort.EF6:对基于EF的应用程序提供了一种便利的方式来进行单元测试。
  • XUnit:.Net上好用的测试框架。
  • Shouldly:断言框架,方便我们书写断言。

2.2. Effort(EF单元测试工具)

It is basically an ADO.NET provider that executes all the data operations on a lightweight in-process main memory database instead of a traditional external database. It provides some intuitive helper methods too that make really easy to use this provider with existing ObjectContext or DbContext classes. A simple addition to existing code might be enough to create data driven tests that can run without the presence of the external database.
简而言之,Effort提供了一个轻量级的内存数据库,来执行所有数据操作。

想对Effort有更对了解,请直接访问Effort Github官方链接。

2.3. xUnit(.Net测试框架)

xUnit专门为.Net Framework打造的一个免费的开源的单元测试工具。
同样,想对Xunit有更对了解,请直接访问xUnit 官方链接。

这里我们就简要介绍下xUnit的基本用法。
xUnit.net 支持两种主要类型的单元测试:facts and theories(事实和理论)。

Facts are tests which are always true. They test invariant conditions.
Theories are tests which are only true for a particular set of data.

Facts:使用[Fact]标记的测试方法,表示不需要传参的常态测试方法。
Theories:使用[Theory]标记的测试方法,表示期望一个或多个DataAttribute实例用来提供参数化测试的方法的参数的值。

首先来看下[Fact]的简单示例:

 

    public class Class1{[Fact]public void PassingTest(){Assert.Equal(4, Add(2, 2));}[Fact]public void FailingTest(){Assert.Equal(5, Add(2, 2));}int Add(int x, int y){return x + y;}}

其中xUnit.Net提供了三种继承于DataAttribute的特性([InlineData]、 [ClassData]、 [PropertyData])用于为[Theory]标记的参数化测试方法传参。
下面是使用这三种特性传参的实例:
InlineData Example

 

public class StringTests1
{[Theory,InlineData("goodnight moon", "moon", true),InlineData("hello world", "hi", false)]public void Contains(string input, string sub, bool expected){var actual = input.Contains(sub);Assert.Equal(expected, actual);}
}

PropertyData Example

 

public class StringTests2
{[Theory, PropertyData("SplitCountData")]public void SplitCount(string input, int expectedCount){var actualCount = input.Split(' ').Count();Assert.Equal(expectedCount, actualCount);}public static IEnumerable<object[]> SplitCountData{get{// Or this could read from a file. :)return new[]{new object[] { "xUnit", 1 },new object[] { "is fun", 2 },new object[] { "to test with", 3 }};}}
}

ClassData Example

 

public class StringTests3
{[Theory, ClassData(typeof(IndexOfData))]public void IndexOf(string input, char letter, int expected){var actual = input.IndexOf(letter);Assert.Equal(expected, actual);}
}public class IndexOfData : IEnumerable<object[]>
{private readonly List<object[]> _data = new List<object[]>{new object[] { "hello world", 'w', 6 },new object[] { "goodnight moon", 'w', -1 }};public IEnumerator<object[]> GetEnumerator(){ return _data.GetEnumerator(); }IEnumerator IEnumerable.GetEnumerator(){ return GetEnumerator(); }
}

2.4. Shouldly(断言框架)

Shouldly提供的断言方式与传统的Assert相比更实用易懂。
对比一下就明白了:

 

Assert.That(contestant.Points, Is.EqualTo(1337));
//Expected 1337 but was 0
contestant.Points.ShouldBe(1337);
//contestant.Points should be 1337 but was 0

首先上写法上更清晰易懂,第二当测试失败时,提示消息也更清楚直接。

同样,想对Shouldly有更对了解,请直接访问Shouldly官方链接。

2.5. 测试基类XxxTestBase

首先来看看代码:

 

public abstract class LearningMpaAbpTestBase : AbpIntegratedTestBase<LearningMpaAbpTestModule>{private DbConnection _hostDb;private Dictionary<int, DbConnection> _tenantDbs; //only used for db per tenant architectureprotected LearningMpaAbpTestBase(){//Seed initial data for hostAbpSession.TenantId = null;UsingDbContext(context =>{new InitialHostDbBuilder(context).Create();new DefaultTenantCreator(context).Create();});//Seed initial data for default tenantAbpSession.TenantId = 1;UsingDbContext(context =>{new TenantRoleAndUserBuilder(context, 1).Create();});LoginAsDefaultTenantAdmin();UsingDbContext(context => new InitialDataBuilder().Build(context));}protected override void PreInitialize(){base.PreInitialize();/* You can switch database architecture here: */UseSingleDatabase();//UseDatabasePerTenant();}/* Uses single database for host and all tenants.*/private void UseSingleDatabase(){_hostDb = DbConnectionFactory.CreateTransient();LocalIocManager.IocContainer.Register(Component.For<DbConnection>().UsingFactoryMethod(() => _hostDb).LifestyleSingleton());}
//...省略后续代码
}

从该段代码中我们可以看出该测试基类继承自AbpIntegratedTestBase<T>
PreInitialize()方法中指定了为租户创建单一数据库还是多个数据库。
_hostDb = DbConnectionFactory.CreateTransient();是Effort提供的方法用来创建的DbConnection(数据库连接)。然后将其使用单例的模式注册到IOC容器中,这样在测试中,所有的数据库连接都将使用Effort为我们创建的数据库连接。
在构造函数中主要做了两件事,预置了初始数据和种子数据,并以默认租户Admin登录。

至此我们对abp为我们默认创建的测试项目有了一个大概的认识。下面我们就开始实战阶段。

3. 单元测试实战

3.1. 理清要测试的方法逻辑

我们以应用服务层的TaskAppService的CreateTask方法为例,创建单元测试。先来看看该方法的代码:

 

public int CreateTask(CreateTaskInput input) {//We can use Logger, it's defined in ApplicationService class.Logger.Info("Creating a task for input: " + input);//判断用户是否有权限if (input.AssignedPersonId.HasValue && input.AssignedPersonId.Value != AbpSession.GetUserId()) PermissionChecker.Authorize(PermissionNames.Pages_Tasks_AssignPerson);var task = Mapper.Map < Task > (input);int result = _taskRepository.InsertAndGetId(task);//只有创建成功才发送邮件和通知if (result > 0) {task.CreationTime = Clock.Now;if (input.AssignedPersonId.HasValue) {task.AssignedPerson = _userRepository.Load(input.AssignedPersonId.Value);var message = "You hava been assigned one task into your todo list.";//TODO:需要重新配置QQ邮箱密码//SmtpEmailSender emailSender = new SmtpEmailSender(_smtpEmialSenderConfig);//emailSender.Send("ysjshengjie@qq.com", task.AssignedPerson.EmailAddress, "New Todo item", message);_notificationPublisher.Publish("NewTask", new MessageNotificationData(message), null, NotificationSeverity.Info, new[] {task.AssignedPerson.ToUserIdentifier()});}}

该方法主要有三步,第一步判断权限,第二步保存数据库并返回Id,第三步发送通知。

3.2. 创建单元测试类并注入依赖

创建TaskAppSerice_Tests类并继承自XxxTestBase类,并注入需要的依赖。

 

public class TaskAppService_Tests : LearningMpaAbpTestBase{private readonly ITaskAppService _taskAppService;public TaskAppService_Tests(){_taskAppService = Resolve<TaskAppService>();}
}

3.3. 创建单元测试方法

第一个方法我们应该测试Happy path(即测试方法的默认场景,没有异常和错误信息)。

 

[Fact] 
public void Should_Create_New_Task_WithPermission() {//Arrange//LoginAsDefaultTenantAdmin();//基类的构造函数中已经以默认租户Admin登录。var initalCount = UsingDbContext(ctx = >ctx.Tasks.Count());var task1 = new CreateTaskInput() {Title = "Test Task",Description = "Test Task",State = TaskState.Open};var task2 = new CreateTaskInput() {Title = "Test Task2",Description = "Test Task2",State = TaskState.Open};//Actint taskResult1 = _taskAppService.CreateTask(task1);int taskResult2 = _taskAppService.CreateTask(task2);//AssertUsingDbContext(ctx = >{taskResult1.ShouldBeGreaterThan(0);taskResult2.ShouldBeGreaterThan(0);ctx.Tasks.Count().ShouldBe(initalCount + 2);ctx.Tasks.FirstOrDefault(t = >t.Title == "Test Task").ShouldNotBe(null);var task = ctx.Tasks.FirstOrDefault(t = >t.Title == "Test Task2");task.ShouldNotBe(null);task.State.ShouldBe(TaskState.Open);});
}

在这里啰嗦一下单元测试的AAA原则:

  • Arrange:为测试做准备工作
  • Act:运行实际测试的代码
  • Assert:断言,校验结果

再说明一下单元测试的方法推荐命名规则:
some_result_occurs_when_doing...

回到我们这个测试方法。
Arrange阶段我们先以Admin登录(Admin具有所有权限),然后获取数据库中初始Task的数量,再准备了两条测试数据。
Act阶段,直接调用TaskAppService的CreateTask方法。
Assert阶段:首先判断CreateTask的返回值大于0 ;再判断现在数据库的数量是否增加了2条;再校验数据库中是否包含创建的Task,并核对Task的状态。

3.4. 预置数据

在进行测试的时候,我们肯定需要一些测试数据,以便我们进行合理的测试。
在基础设施层,我们有专门的SeedData目录用来预置种子数据。但是进行单元测试的测试数据不应该污染实体数据库,所以直接在SeedData目录预置数据就不太现实。

3.4.1. 创建TestDataBuilder

所以,我们就直接在测试项目中,新建一个TestDatas文件夹来管理测试种子数据。
然后创建TestDataBuilder类,通过该类来统一创建所需的测试数据。(注意,需要修改下类中的_context类型为你自己项目对应的DbContext)

 

namespace LearningMpaAbp.Tests.TestDatas
{public class TestDataBuilder{private readonly LearningMpaAbpDbContext _context;private readonly int _tenantId;public TestDataBuilder(LearningMpaAbpDbContext context, int tenantId){_context = context;_tenantId = tenantId;}public void Create(){_context.DisableAllFilters();//new TestUserBuilder(_context,_tenantId).Create();//new TestTasksBuilder(_context,_tenantId).Create();_context.SaveChanges();}}
}

然后修改我们的测试基类XxxTestBase,在构造函数调用我们新建的TestDataBuilderCreate()方法。new TestDataBuilder(context, 1).Create();,如下图:

3.4.2. 创建Task测试数据

创建TestTasksBuilder,如下:(注意,需要修改下类中的_context类型为你自己项目对应的DbContext)

 

namespace LearningMpaAbp.Tests.TestDatas
{public class TestTasksBuilder{private readonly LearningMpaAbpDbContext _context;private readonly int _tenantId;public TestTasksBuilder(LearningMpaAbpDbContext context, int tenantId){_context = context;_tenantId = tenantId;}public void Create(){for (int i = 0; i < 8; i++){var task = new Task(){Title = "TestTask" + i,Description = "Test Task " + i,CreationTime = DateTime.Now,State = (TaskState)new Random().Next(0, 1)};_context.Tasks.Add(task);}}}
}

然后再在TestDataBuild中调用该类的Create()的方法即可。
new TestTasksBuilder(_context,_tenantId).Create();

3.5. Run the test(单元测试跑起来)

UT Passed

喜闻乐见的绿色,单元测试通过。

3.6. 完善测试用例

单元测试中我们仅仅测试Happy Path是远远不够的。因为毕竟我们只是测试了正常的正确场景。为了提高单元测试的覆盖度,我们应该针对代码可能出现的异常问题进行测试。
还拿我们刚刚的CreateTask方法为例,其中第二步有一个验证权限操作,当用户没有权限的时候,Task应该不能创建并抛出异常。那我们就针对无权限的场景补充一个单元测试吧。

3.6.1. 预置数据

无权限简单,直接创建一个新用户登录就ok了。但为了用户复用,我们还是在种子数据中预置测试用户吧。

回到我们的TestDatas目录,创建TestUserBuilder,来预置测试用户。

 

namespace LearningMpaAbp.Tests.TestDatas
{/// <summary>/// 预置测试用户(无权限)/// </summary>public class TestUserBuilder{private readonly LearningMpaAbpDbContext _context;private readonly int _tenantId;public TestUserBuilder(LearningMpaAbpDbContext context, int tenantId){_context = context;_tenantId = tenantId;}public void Create(){var testUser =_context.Users.FirstOrDefault(u => u.TenantId == _tenantId && u.UserName == "TestUser");if (testUser == null){testUser = new User{TenantId = _tenantId,UserName = "TestUser",Name = "Test User",Surname = "Test",EmailAddress = "test@defaulttenant.com",Password = User.DefaultPassword,IsEmailConfirmed = true,IsActive = true};_context.Users.Add(testUser);}}}
}

然后再在TestDataBuild中调用该类的Create()的方法即可。
new TestUserBuilder(_context,_tenantId).Create();

3.6.2. 完善单元测试

 

/// <summary>
/// 若没有分配任务给他人的权限,创建的任务指定给他人,则任务创建不成功。
/// </summary>
[Fact]
public void Should_Not_Create_New_Order_AssignToOrther_WithoutPermission()
{//ArrangeLoginAsTenant(Tenant.DefaultTenantName, "TestUser");//获取admin用户var adminUser = UsingDbContext(ctx => ctx.Users.FirstOrDefault(u => u.UserName == User.AdminUserName));var newTask = new CreateTaskInput(){Title = "Test Task",Description = "Test Task",State = TaskState.Open,AssignedPersonId = adminUser.Id //TestUser创建Task并分配给Admin};//Act,AssertAssert.Throws<AbpAuthorizationException>(() => _taskAppService.CreateTask(newTask));}

当用户无权限时,将抛出Abp封装的AbpAuthorizationException(未授权异常)。

UT Passed

单元测试用例,就讲这两个,剩下的自己动手完善吧。源码中已经覆盖测试,可供参考。

4. 总结

这篇文章中主要梳理了Abp中如何进行单元测试,以及依赖的xUnit、Effort、Shouldly框架的用法。并基于以上内容的总结,进行了单元测试的实战演练。
相信看完此篇文章的总结,对你在Abp中进行单元测试,有所裨益。



作者:圣杰
链接:https://www.jianshu.com/p/4876599247d5
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/438943.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

etl构建数据仓库五步法_带你了解数据仓库的基本架构

数据仓库的目的是构建面向分析的集成化数据环境&#xff0c;为企业提供决策支持&#xff08;Decision Support&#xff09;。其实数据仓库本身并不“生产”任何数据&#xff0c;同时自身也不需要“消费”任何的数据&#xff0c;数据来源于外部&#xff0c;并且开放给外部应用&a…

ABP入门系列(12)——如何升级Abp并调试源码

1. 升级Abp 本系列教程是基于Abp V1.0版本&#xff0c;现在Abp版本已经升级至V1.4.2&#xff0c;其中新增了New Feature&#xff0c;并对Abp做了相应的Enhancements&#xff0c;以及Bug fixs。现在我们就把它升级至最新版本&#xff0c;那如何升级呢&#xff1f; 下面就请按我…

聚类分析在用户行为中的实例_看完这篇,你还敢说不懂聚类分析?

点击上方蓝色字关注我们~大数据分析中的应用&#xff0c;最常用的经典算法之一就是聚类法&#xff0c;这是数据挖掘采用的起步技术&#xff0c;也是数据挖掘入门的一项关键技术。什么是聚类分析&#xff1f;聚类分析有什么用&#xff1f;聚类算法有哪些&#xff1f;聚类分析的应…

ABP入门系列(13)——Redis缓存用起来

1. 引言 创建任务时我们需要指定分配给谁&#xff0c;Demo中我们使用一个下拉列表用来显示当前系统的所有用户&#xff0c;以供用户选择。我们每创建一个任务时都要去数据库取一次用户列表&#xff0c;然后绑定到用户下拉列表显示。如果就单单对一个demo来说&#xff0c;这样实…

ABP入门系列(14)——应用BootstrapTable表格插件

1. 引言 之前的文章ABP入门系列&#xff08;7&#xff09;——分页实现讲解了如何进行分页展示&#xff0c;但其分页展示仅适用于前台web分页&#xff0c;在后台管理系统中并不适用。后台管理系统中的数据展示一般都是使用一些表格插件来完成的。这一节我们就使用BootstrapTab…

musictools怎么用不了_夏天少不了一只草编包,怎么搭配才不像“买菜用”?

要说有什么包包能跟夏天的气息一拍即合&#xff0c;那绝对非“草编包”莫属&#xff01;由藤条、柳条等编制而成的田园风“小篮子”&#xff0c;已经成了夏日街头小姐姐们的包包首选。各大品牌都争相推出草编包系列&#xff0c;在原有的浪漫度假风之外&#xff0c;添加了更多时…

ABP入门系列(15)——创建微信公众号模块

1. 引言 现在的互联网已不在仅仅局限于网页应用&#xff0c;IOS、Android、平板、智能家居等平台正如火如荼的迅速发展&#xff0c;移动应用的需求也空前旺盛。所有的互联网公司都不想错过这一次移动浪潮&#xff0c;布局移动市场分一份移动红利。 的确&#xff0c;智能手机作…

spark 算子例子_Spark性能调优方法

公众号后台回复关键词&#xff1a;pyspark&#xff0c;获取本项目github地址。Spark程序可以快如闪电⚡️&#xff0c;也可以慢如蜗牛?。它的性能取决于用户使用它的方式。一般来说&#xff0c;如果有可能&#xff0c;用户应当尽可能多地使用SparkSQL以取得更好的性能。主要原…

ABP入门系列(16)——通过webapi与系统进行交互

1. 引言 上一节我们讲解了如何创建微信公众号模块&#xff0c;这一节我们就继续跟进&#xff0c;来讲一讲公众号模块如何与系统进行交互。 微信公众号模块作为一个独立的web模块部署&#xff0c;要想与现有的【任务清单】进行交互&#xff0c;我们要想明白以下几个问题&#x…

python嵩天第二版第五章_如何避免从入门到放弃——python小组学习复盘

2019年春节python学习行动复盘2019-02-09为了主攻python&#xff0c;没有参加心理学晨读。对心理学也不敢兴趣&#xff0c;怕耽误学习python的时间。那么没学习心理学的情况下&#xff0c;python学的怎么样&#xff1f;是否达到自己的预期&#xff1f;一、预期目标&#xff1a;…

ABP入门系列(17)——使用ABP集成的邮件系统发送邮件

1.Abp集成的邮件模块是如何实现的 ABP中对邮件的封装主要集成在Abp.Net.Mail和Abp.Net.Mail.Smtp命名空间下&#xff0c;相应源码在此。 分析可以看出主要由以下几个核心类组成&#xff1a; EmailSettingNames&#xff1a;静态常量类&#xff0c;主要定义了发送邮件需要的相关…

cdn转发防攻击_高防CDN和高防服务器的区别?

越来越多的网络攻击需要处理&#xff0c;而高防CDN和高防服务器是很好的选择&#xff0c;那么如何选择呢&#xff1f;我们就来分析一下关于这两者之间的选择。首先从价格上看的话&#xff0c;高防御CDN的价格相对高一些&#xff0c;防御上看&#xff0c;高防御CDN的防御效果也更…

ABP入门系列(18)—— 使用领域服务

1.引言 自上次更新有一个多月了&#xff0c;发现越往下写&#xff0c;越不知如何去写。特别是当遇到DDD中一些概念术语的时候&#xff0c;尤其迷惑。如果只是简单的去介绍如何去使用ABP&#xff0c;我只需参照官方文档&#xff0c;实现到任务清单Demo中去就可以了&#xff0c;…

mysql文件类型_MyCat教程:实现MySql主从复制

原文&#xff1a;http://iii75.cn/mwQhBW 作者&#xff1a;波波烤鸭历史相关文章Mycat入门教程单个mysql数据库在处理业务的时候肯定是有限的&#xff0c;这时我们扩展数据库的第一种方式就是对数据库做读写分离(主从复制),本文我们就先来介绍下怎么来实现mysql的主从复制操作。…

截屏当前界面_电脑屏幕怎么截取,常见的几种电脑截屏方法

随着科技的快速发展电脑已经逐渐渗入到我们的工作和生活中&#xff0c;我们需要使用电脑的地方也越来越多&#xff0c;电脑已经成为了一种新式的办公工具。今天小编不是向大家介绍电脑的应用&#xff0c;而是想要和大家分享一下关于电脑截图的几种方法。1、Print Screen SysRqP…

ABP入门系列(19)——使用领域事件

1.引言 最近刚学习了下DDD中领域事件的理论知识&#xff0c;总的来说领域事件主要有两个作用&#xff0c;一是解耦&#xff0c;二是使用领域事件进行事务的拆分&#xff0c;通过引入事件存储&#xff0c;来实现数据的最终一致性。若想了解DDD中领域事件的概念&#xff0c;可参…

扩容是元素还是数组_Java中对数组的操作

数组对于每一门编程语言来说都是重要的数据结构之一&#xff0c;当然不同语言对于数组的实现及处理也不尽相同。Java语言中提供的数组是用来存储固定大小的同类型元素。如&#xff1a;声明一个数组变量&#xff0c;numbers[100]来代替直接声明100个独立变量number0,number1,...…

ABP入门系列(20)——使用后台作业和工作者

1.引言 说到后台作业&#xff0c;你可能条件反射的想到BackgroundWorker&#xff0c;但后台作业并非是后台任务&#xff0c;后台作业用一种队列且持久稳固的方式安排一些待执行后台任务。 为执行长时间运行的任务而用户无需等待&#xff0c;以提高用户体验。为创建可重试且持…

加载中_GIS地图在项目中的加载显示

下面我们就来说说如何在应用程序中加载显示GIS地图&#xff0c;首先我们在SuperMap iDesktop 9D(10i)中编辑好我们需要的地图&#xff0c;如下图所示&#xff1a;如上图所示&#xff0c;这是我编辑好的一幅天河区的地图&#xff0c;下面我就以这幅地图为例来说说如何把这样一幅…

ABP入门系列(21)——切换MySQL数据库

1. 引言 Abp支持MySql已经不是什么新鲜事了&#xff0c;但按照官方文档&#xff1a;Entity Framework - MySql Integration来&#xff0c;你未必能成功切换&#xff0c;本文就记录下切换MySql数据库遇到的一些坑&#xff0c;供后人乘凉&#xff01; 2. 环境准备 MySql数据库…