返回总目录《一步一步使用ABP框架搭建正式项目系列教程》


本篇目录

介绍###

在这篇博客中,我们来说说基于ABP项目的单元测试。说到单元测试(Unit Test),估计很多人只有在上《软件工程》这门课时才接触过这个概念,平时写代码基本不写测试的,测试的唯一办法就是代码写完后跑一遍,看看符不符合预期的效果,如果符合就算完成任务了。但是,在大公司或者项目比较大(比如开发一个框架)的时候,单元测试很重要,它是保证软件质量的一个重要指标。

在这篇博客中,我会在同一个解决方案中创建一个测试项目,而不是另外创建一个新的解决方案。解决方案的结构如下所示:

我将会测试该项目的应用服务,包括LcErp.Application,LcErp.Core,LcErp.EntityFramework子项目。至于如何使用ABP框架搭建项目,您可以参考之前的文章,本篇单讲测试话题。

创建测试项目###

如果你是用ABP启动模板创建的项目,那么它会自动创建测试项目的,否则,你可以手动创建一个测试项目。比如,我这里创建了一个叫做LcErp.Tests的类库项目,它位于Tests文件夹下。如果你是手动添加的类库项目,请添加下面的nuget包:

  • Abp.TestBase:提供了一些使得测试基于ABP框架应用的测试更为简单的基类。
  • Abp.EntityFramework:使用EF作为ORM。
  • Effort.EF6:使得创建一个伪造的、供EF容易使用的内存数据库成为可能。
  • xunit:这是我们使用的测试框架。此外,也添加了在VS中运行测试的 xunit.runner.visualstudio
  • Shouldly:该包是为了方便书写断言的。

当我们添加了这些包之后,它们的依赖包也会自动添加到项目中。最后,我们要将LcErp.Application,LcErp.Core,LcErp.EntityFramework的引用添加到LcErp项目中,因为我们要测试这些项目。

准备测试基类###

为了使创建测试类更简单,我们要先创建一个基类,该基类准备了一个伪造的数据库连接:

/// <summary>
/// 这是我们所有测试类的基类。
/// 它准备了ABP系统,模块和一个伪造的内存数据库。
/// 具有初始数据的种子数据库。
/// 提供了容易使用的方法<see cref="LcErpDbContext"/>
/// </summary>
public abstract class AppTestBase : AbpIntegratedTestBase
{
protected AppTestBase()
{
//Seed initial data
UsingDbContext(context =>
{
new InitialDbBuilder(context).Create();
new TestDataBuilder(context).Create();
}); LoginAsDefaultTenantAdmin();
} protected override void PreInitialize()
{
base.PreInitialize(); //Fake DbConnection using Effort!
LocalIocManager.IocContainer.Register(
Component.For<DbConnection>()
.UsingFactoryMethod(DbConnectionFactory.CreateTransient)
.LifestyleSingleton()
);
} protected override void AddModules(ITypeList<AbpModule> modules)
{
base.AddModules(modules); //Adding testing modules. Depended modules of these modules are automatically added.
modules.Add<LcErpTestModule>();
} #region UsingDbContext protected void UsingDbContext(Action<LcErpDbContext> action)
{
using (var context = LocalIocManager.Resolve<LcErpDbContext>())
{
context.DisableAllFilters();
action(context);
context.SaveChanges();
}
} protected async Task UsingDbContextAsync(Action<LcErpDbContext> action)
{
using (var context = LocalIocManager.Resolve<LcErpDbContext>())
{
context.DisableAllFilters();
action(context);
await context.SaveChangesAsync();
}
} protected T UsingDbContext<T>(Func<LcErpDbContext, T> func)
{
T result; using (var context = LocalIocManager.Resolve<LcErpDbContext>())
{
context.DisableAllFilters();
result = func(context);
context.SaveChanges();
} return result;
} protected async Task<T> UsingDbContextAsync<T>(Func<LcErpDbContext, Task<T>> func)
{
T result; using (var context = LocalIocManager.Resolve<LcErpDbContext>())
{
context.DisableAllFilters();
result = await func(context);
await context.SaveChangesAsync();
} return result;
} #endregion ......这里省略其他方法...

该基类继承了AbpIntegratedTestBase,它是一个初始化了ABP系统的基类,定义了protected IIocManager LocalIocManager { get; }。每个测试都会使用这个专用的IIocManager。因此,测试之间是相互隔离的。

我们重写了AddModules方法来添加我们想要测试的模块(依赖的模块会自动添加)。

在PreInitialize中,我们使用EffortDbConnection注册到依赖注入系统中,注册类型为Singleton。因此,即使我们在相同的测试中创建了不止一个DbContext,也会在一个测试中使用相同的数据库(和连接)。为了使用该内存数据库,LcErp必须有一个获取DbConnection的构造函数。因此,数据库上下文LcErp类中的构造函数会多一个,如下:

/* This constructor is used in tests to pass a fake/mock connection.
*/
public LcErpDbContext(DbConnection dbConnection)
: base(dbConnection, true)
{ }

在AppTestBase的构造函数中,我们也在数据库中创建了一个初始化数据(initial data)。这是很重要的,因为一些测试要求数据库中存在的数据。InitialDbBuilder类填充数据库的内容如下(详细信息可自行查看项目):


public class InitialDbBuilder
{
private readonly LcErpDbContext _context; public InitialDbBuilder(LcErpDbContext context)
{
_context = context;
} public void Create()
{
_context.DisableAllFilters(); new DefaultEditionCreator(_context).Create();
new DefaultLanguagesCreator(_context).Create();
new DefaultTenantRoleAndUserCreator(_context).Create();
new DefaultSettingsCreator(_context).Create(); _context.SaveChanges();
}
}

AppTestBase的UsingDbContext方法使得当需要直接使用DbContext连接数据库时创建DbContext更容易。在构造函数中我们使用了它,接下来我们将会在测试中看到如何使用它。

我们所有的测试类都会从AppTestBase继承。因此,所有的测试都会通过初始化ABP启动,使用一个具有初始化数据的伪造数据库。为使测试更容易,我们也可以给这个基类添加通用的帮助方法。

创建第一个测试###

接下来,我们正式创建第一个单元测试。下面的ProductionOrderAppService类中有一个CreateOrder方法,定义如下:

public class ProductionOrderAppService : LcErpAppServiceBase, IProductionOrderAppService
{
private readonly IRepository<Order> _orderRepository;
public ProductionOrderAppService(IRepository<Order> orderRepository)
{
_orderRepository = orderRepository;
} public void CreateOrder(CreateOrderInput input)
{
var order = input.MapTo<Order>();//将dto对象映射为实体对象
_orderRepository.Insert(order);
} ......其他方法
}

一般来说,单元测试中,测试类的依赖是假的(通过使用一些模仿框架如Moq和NSubstitute来创建伪造的实现)。这使得单元测试更加困难,特别是当依赖逐渐增多时。

我们这里不会这样处理,因为我们使用了依赖注入,所有的依赖会通过具有真实实现的依赖注入自动填充,而不是伪造。我们伪造的东西只有数据库。实际上,这是一个集成测试,因为它不仅测试了ProductionOrderAppService,还测试了仓储,甚至我们测试了验证,工作单元和ABP的其他基础设施。这是非常具有价值的,因为我们正在更加真实地测试这个应用程序。

现在,我们开始创建第一个测试来测试CreateOrder 方法。

public class ProductionOrderAppService_Tests:AppTestBase
{
private readonly IProductionOrderAppService _orderAppService; public ProductionOrderAppService_Tests()
{
//创建被测试的类(SUT-Software Under Test[被测系统])
_orderAppService = LocalIocManager.Resolve<IProductionOrderAppService>();
} [Fact]
public void Should_Create_New_Order()
{
//准备测试
var initialCount = UsingDbContext(ctx => ctx.Orders.Count()); //运行被测系统
_orderAppService.CreateOrder(new CreateOrderInput
{
Amount = 10,
CustomerId = 10,
OrderId = "abc",
OrderrDateTime = DateTime.Now,
OrderUserId = 10,
Sum = 10,
Remark = "测试一"
}); _orderAppService.CreateOrder(new CreateOrderInput
{
OrderId = "efd",
Remark = "测试二"
}); //校验结果 UsingDbContext(ctx =>
{
ctx.Orders.Count().ShouldBe(initialCount+2);
ctx.Orders.FirstOrDefault(o=>o.Remark=="测试一").ShouldNotBe(null);
var order2 = ctx.Orders.FirstOrDefault(o => o.OrderId == "efd");
order2.ShouldNotBe(null);
order2.Remark.ShouldBe("测试二");
//Assert.Equal("测试二",order2.Remark); }); } }

正如之前所讲,我们继承了AppTestBase这个测试基类。在一个单元测试中,我们首先应该创建被测试的对象。在上面的构造函数中,使用LocalIocManager(依赖注入管理者)来创建了一个 IProductionOrderAppService(因为ProductionOrderAppService实现了IProductionOrderAppService,所以会创建ProductionOrderAppService)。通过这种方法,就避免了创建伪造的依赖实现。

Should_Create_New_Order是测试方法。它使用了xUnit的 Fact特性进行修饰。这样,xUnit就理解了这是个测试方法,然后运行这个方法。

在一个测试方法中,我们一般遵循包含三步骤的AAA模式:

  1. Arrange:为测试准备
  2. Act:运行SUT(实际测试的代码)
  3. Assert:校验结果

在Should_Create_New_Order方法中,我们创建了2个订单,因此,我们的三步骤是:

  1. Arrange:我们获取数据库中的订单总数量
  2. Act:使用_orderAppService.CreateOrder添加了2个订单
  3. Assert:检查订单数量是否增加了2个。同时尝试从数据库中获取订单,以检查订单是否被正确地插入到数据库中。

这里,我们使用了UsingDbContext方法来直接使用DbContext。如果测试成功,我们就知道了当输入合理时,CreateOrder方法可以创建订单。

要运行测试,我们要打开VS的测试管理器,选择测试->窗口->测试资源管理器(如果没有找到刚才创建的测试类和方法,先保存生成一下):



选中刚才创建的测试,右键“运行该测试”:

如上所示,我们的第一个单元测试通过了。恭喜恭喜!如果测试或者测试代码不正确,那么测试会失败!

假设我注释掉第二个订单对象的Remark的赋值,然后再次运行测试,结果会失败:

Shouldly类库使得失败信息更加清晰,也使得编写断言更加容易。比较一下xUnit的 Assert.Equal和 Shouldly的 ShouldBe扩展方法:

order2.Remark.ShouldBe("测试二");//使用Shouldly
Assert.Equal("测试二",order2.Remark);//使用xUnit的Assert

第一个读写更简单且自然,并且Shouldly提供了很多其他的扩展方法来方便我们的编程,请查看Shouldly相应的文档。

测试异常###

我想为CreateOrder方法再创建一个测试方法,但是,这次输入不合法

[Fact]
public void Should_Not_Create_New_Order_WithoutOrderId()
{
Assert.Throws<AbpValidationException>(() => _orderAppService.CreateOrder(new CreateOrderInput
{
Remark = "该订单的OrderId没有赋值"
}));
}

如果没有为创建的订单的OrderId属性赋值,那么我期望CreateOrder会抛异常。因为在CreateOrderInput DTO类中,OrderId被标记为 Required,所以,如果CreateOrder抛出异常,测试就会成功,否则失败。注意:验证输入和抛异常是ABP基础设施处理的。

测试结果如下:

在测试中使用仓储###

下面在测试方法中使用仓储,改造上面创建订单的测试方法:

        [Fact]
public void Should_Create_New_Order()
{
//准备测试
//var initialCount = UsingDbContext(ctx => ctx.Orders.Count());
//使用仓储代替DbContext
var orderRepo = LocalIocManager.Resolve<IRepository<Order>>(); //运行被测系统
_orderAppService.CreateOrder(new CreateOrderInput
{
Amount = 10,
CustomerId = 10,
OrderId = "abc",
OrderrDateTime = DateTime.Now,
OrderUserId = 10,
Sum = 10,
Remark = "测试一"
}); _orderAppService.CreateOrder(new CreateOrderInput
{
OrderId = "efd",
Remark = "测试二"
}); //校验结果 //UsingDbContext(ctx =>
//{
// ctx.Orders.Count().ShouldBe(initialCount+2);
// ctx.Orders.FirstOrDefault(o=>o.Remark=="测试一").ShouldNotBe(null);
// var order2 = ctx.Orders.FirstOrDefault(o => o.OrderId == "efd");
// order2.ShouldNotBe(null);
// order2.Remark.ShouldBe("测试二");
// //Assert.Equal("测试二",order2.Remark);
//}); orderRepo.GetAllList().Count.ShouldBe(2); }

测试异步方法###

我们也可以使用xUnit测试异步方法。比如,ProductionOrderAppService的GetAllOrders方法是异步方法,那么测试方法也应该是异步的(async)。

[Fact]
public async Task Should_Get_All_People()
{
var output = await _orderAppService.GetAllPeople();
output.People.Count.ShouldBe(4);
}

小结###

这篇文章中,我只想展示一下基于ABP框架搭建的项目的测试。ABP提供了一个很好的基础设施来实现测试驱动开发(TDD),或者为你的应用程序简单地创建一些单元测试或集成测试。

Effort类库提供了一个伪造的数据库,它和EF协作地很好。只要你使用了EF或者Linq来执行数据库操作,它就会工作。如果你使用了存储过程,并想测试它,那么Effort不支持。对于这些情况,建议使用LocalDB。

使用xUnit,EF,Effort和ABP进行单元测试(C#)的更多相关文章

  1. 如何使用ASP.NET Core、EF Core、ABP(ASP.NET Boilerplate)创建分层的Web应用程序(第一部分)

    本文是为了学习ABP的使用,是翻译ABP官方文档的一篇实战教程,我暂时是优先翻译自己感兴趣或者比较想学习的部分,后续有时间希望能将ABP系列翻译出来,除了自己能学习外,有可能的话希望帮助一些英文阅读能 ...

  2. ABP中单元测试的技巧:Mock和数据驱动

    (此文章同时发表在本人微信公众号"dotNET每日精华文章",欢迎右边二维码来关注.) 题记:虽然ABP为大家提供了测试的脚手架了,不过有些小技巧还是需要自己探索的. ASP.NE ...

  3. 使用xUnit为.net core程序进行单元测试(上)

    一. 导读 为什么要编写自动化测试程序(Automated Tests)? 可以频繁的进行测试 可以在任何时间进行测试,也可以按计划定时进行,例如:可以在半夜进行自动测试. 肯定比人工测试要快. 可以 ...

  4. 使用xUnit为.net core程序进行单元测试(1)

    导读 为什么要编写自动化测试程序(Automated Tests)? 可以频繁的进行测试 可以在任何时间进行测试,也可以按计划定时进行,例如:可以在半夜进行自动测试. 肯定比人工测试要快. 可以更快速 ...

  5. 使用 xUnit 编写 ASP.NET Core WebAPI单元测试

    本文使用xUnit对ASP.NET Core WebAPI做单元测试,使用HttpClient的同步和异步请求,下面详细介绍xUnit的使用过程: 一.创建示例项目 模板为我们自动创建了一个Value ...

  6. 使用xUnit为.net core程序进行单元测试(中)

    第一部分: http://www.cnblogs.com/cgzl/p/8283610.html 下面有一点点内容是重叠的.... String Assert 测试string是否相等: [Fact] ...

  7. 使用xUnit为.net core程序进行单元测试(3)

    第1部分: http://www.cnblogs.com/cgzl/p/8283610.html 第2部分: http://www.cnblogs.com/cgzl/p/8287588.html 请使 ...

  8. 使用xUnit为.net core程序进行单元测试(4)

    第1部分: http://www.cnblogs.com/cgzl/p/8283610.html 第2部分: http://www.cnblogs.com/cgzl/p/8287588.html 第3 ...

  9. 使用xUnit为.net core程序进行单元测试 -- Assert

    第一部分: http://www.cnblogs.com/cgzl/p/8283610.html Assert Assert做什么?Assert基于代码的返回值.对象的最终状态.事件是否发生等情况来评 ...

随机推荐

  1. react组件的生命周期

    写在前面: 阅读了多遍文章之后,自己总结了一个.一遍加强记忆,和日后回顾. 一.实例化(初始化) var Button = React.createClass({ getInitialState: f ...

  2. 【每日一linux命令3】参数(或称选项)顺序

    一般除了特殊情况,参数是没有顺序的.举例而言,输入"–a –v"与输入"–v –a"以及"–av" 的执行效果是相同的.但若该参数后指定了要 ...

  3. 再谈CAAnimation动画

    CAAnimaton动画分为CABasicAnimation & CAKeyframeAnimation CABasicAnimation动画, 顾名思义就是最基本的动画, 老规矩先上代码: ...

  4. JAVA问题集锦Ⅰ

    1.Java的日期添加: import java.util.Date ; date=new date();//取时间 Calendar calendar = new GregorianCalendar ...

  5. 手动添加kdump

    背景:     Linux嵌入式设备内核挂死后,无法自动重启,需要手动重启.而且如果当时没有连串口的话,就无法记录内核挂死时的堆栈,所以需要添加一种方式来记录内核挂死信息方便以后调试使用.设备中增加k ...

  6. [C#][算法] 用菜鸟的思维学习算法 -- 马桶排序、冒泡排序和快速排序

    用菜鸟的思维学习算法 -- 马桶排序.冒泡排序和快速排序 [博主]反骨仔 [来源]http://www.cnblogs.com/liqingwen/p/4994261.html  目录 马桶排序(令人 ...

  7. 你真的会玩SQL吗?之逻辑查询处理阶段

    你真的会玩SQL吗?系列目录 你真的会玩SQL吗?之逻辑查询处理阶段 你真的会玩SQL吗?和平大使 内连接.外连接 你真的会玩SQL吗?三范式.数据完整性 你真的会玩SQL吗?查询指定节点及其所有父节 ...

  8. Take into Action!

    很久没有认真地写文字了. 刚毕业一两年断断续续在csdn上写过一些当时的工作记录,然后没有坚持下去.有时候是觉得自己不牛,记录的东西旁人看起来也许不值一提:有时候觉得结婚生娃了,然后时间不够用(确实是 ...

  9. 电信计费业务:预后融合OCS到底应该实扣还是虚扣?

    引入OCS的初衷之一是为了让计费系统能够参与到用户的通讯控制中来,也就是所谓的实时信控.用户在没有余额时,通讯就会被停止,不会造成"天价欠费 ",一方面保障用户的利益,一方面也保障 ...

  10. Android Fragment 剖析

    1.Fragment如何产生?2.什么是Fragment Android运行在各种各样的设备中,有小屏幕的手机,超大屏的平板甚至电视.针对屏幕尺寸的差距,很多情况下,都是先针对手机开发一套App,然后 ...