本文共 9830 字,大约阅读时间需要 32 分钟。
在这篇博客中,我们来说说基于ABP项目的单元测试。说到单元测试(Unit Test),估计很多人只有在上《软件工程》这门课时才接触过这个概念,平时写代码基本不写测试的,测试的唯一办法就是代码写完后跑一遍,看看符不符合预期的效果,如果符合就算完成任务了。但是,在大公司或者项目比较大(比如开发一个框架)的时候,单元测试很重要,它是保证软件质量的一个重要指标。
在这篇博客中,我会在同一个解决方案中创建一个测试项目,而不是另外创建一个新的解决方案。解决方案的结构如下所示:
我将会测试该项目的应用服务,包括LcErp.Application,LcErp.Core,LcErp.EntityFramework子项目。至于如何使用ABP框架搭建项目,您可以参考之前的文章,本篇单讲测试话题。
如果你是用ABP启动模板创建的项目,那么它会自动创建测试项目的,否则,你可以手动创建一个测试项目。比如,我这里创建了一个叫做LcErp.Tests的类库项目,它位于Tests文件夹下。如果你是手动添加的类库项目,请添加下面的nuget包:
当我们添加了这些包之后,它们的依赖包也会自动添加到项目中。最后,我们要将LcErp.Application,LcErp.Core,LcErp.EntityFramework的引用添加到LcErp项目中,因为我们要测试这些项目。
为了使创建测试类更简单,我们要先创建一个基类,该基类准备了一个伪造的数据库连接:
////// 这是我们所有测试类的基类。 /// 它准备了ABP系统,模块和一个伪造的内存数据库。 /// 具有初始数据的种子数据库。 /// 提供了容易使用的方法 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/// () .UsingFactoryMethod(DbConnectionFactory.CreateTransient) .LifestyleSingleton() ); } protected override void AddModules(ITypeList modules) { base.AddModules(modules); //Adding testing modules. Depended modules of these modules are automatically added. modules.Add (); } #region UsingDbContext protected void UsingDbContext(Action action) { using (var context = LocalIocManager.Resolve ()) { context.DisableAllFilters(); action(context); context.SaveChanges(); } } protected async Task UsingDbContextAsync(Action action) { using (var context = LocalIocManager.Resolve ()) { context.DisableAllFilters(); action(context); await context.SaveChangesAsync(); } } protected T UsingDbContext (Func func) { T result; using (var context = LocalIocManager.Resolve ()) { context.DisableAllFilters(); result = func(context); context.SaveChanges(); } return result; } protected async Task UsingDbContextAsync (Func > func) { T result; using (var context = LocalIocManager.Resolve ()) { context.DisableAllFilters(); result = await func(context); await context.SaveChangesAsync(); } return result; } #endregion ......这里省略其他方法...
该基类继承了AbpIntegratedTestBase,它是一个初始化了ABP系统的基类,定义了protected IIocManager LocalIocManager { get; }
。每个测试都会使用这个专用的IIocManager。因此,测试之间是相互隔离的。
我们重写了AddModules方法来添加我们想要测试的模块(依赖的模块会自动添加)。
在PreInitialize中,我们使用Effort将 DbConnection注册到依赖注入系统中,注册类型为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_orderRepository; public ProductionOrderAppService(IRepository orderRepository) { _orderRepository = orderRepository; } public void CreateOrder(CreateOrderInput input) { var order = input.MapTo ();//将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(); } [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模式:
在Should_Create_New_Order方法中,我们创建了2个订单,因此,我们的三步骤是:
这里,我们使用了UsingDbContext方法来直接使用DbContext。如果测试成功,我们就知道了当输入合理时,CreateOrder方法可以创建订单。
要运行测试,我们要打开VS的测试管理器,选择测试->窗口->测试资源管理器(如果没有找到刚才创建的测试类和方法,先保存生成一下):
选中刚才创建的测试,右键“运行该测试”:如上所示,我们的第一个单元测试通过了。恭喜恭喜!如果测试或者测试代码不正确,那么测试会失败!
假设我注释掉第二个订单对象的Remark的赋值,然后再次运行测试,结果会失败:
Shouldly类库使得失败信息更加清晰,也使得编写断言更加容易。比较一下xUnit的 Assert.Equal和 Shouldly的 ShouldBe扩展方法:
order2.Remark.ShouldBe("测试二");//使用ShouldlyAssert.Equal("测试二",order2.Remark);//使用xUnit的Assert
第一个读写更简单且自然,并且Shouldly提供了很多其他的扩展方法来方便我们的编程,请查看Shouldly相应的文档。
我想为CreateOrder方法再创建一个测试方法,但是,这次输入不合法:
[Fact]public void Should_Not_Create_New_Order_WithoutOrderId(){ Assert.Throws(() => _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>(); //运行被测系统 _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。
本文转自tkbSimplest博客园博客,原文链接:http://www.cnblogs.com/farb/p/ABPPracticeUnitTest.html,如需转载请自行联系原作者