使用xUnit,EF,Effort和ABP进行单元测试(C#)

简介:

本篇目录

介绍

在这篇博客中,我们来说说基于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中,我们使用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<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。






本文转自tkbSimplest博客园博客,原文链接:http://www.cnblogs.com/farb/p/ABPPracticeUnitTest.html,如需转载请自行联系原作者

目录
相关文章
|
测试技术 API C#
C# 软件开发之单元测试
在日常开发中,一般通过启动调试或运行程序来查看功能是否符合预期,如果不符合预期,则需要优化程序,再次运行,如此反复,直到程序的输出符合预期需求为止。随着程序的不断复杂化,某些功能的测试也变得越来越复杂,可能为了验证一个很小的改动项,就需要操作很多步骤,才能验证成功,如果验证不成功,则需要多次重复验证,这对于开发者来说,将大大的拖延了开发进度。如何才能将复杂的功能进行拆分,每一个都可以单独进行验证呢?如果其他的功能没有问题,则只需要验证修改的那部分内容即可,这就是本篇文章需要介绍的单元测试。通过创建和运行单元测试,检查代码是否按预期工作。
340 1
|
JavaScript 测试技术 C#
【C#】【xUnit】【Moq】.NET单元测试Mock框架Moq初探!
在TDD开发模型中,经常是在编码的同时进行单元测试的编写,由于现代软件开发不可能是一个人完成的工作,所以在定义好接口的时候我们就可以进行自己功能的开发(接口不能经常变更),而我们调用他人的功能时只需要使用接口即可。
5352 0
|
2月前
|
测试技术 C# 数据库
C# 单元测试框架 NUnit 一分钟浅谈
【10月更文挑战第17天】单元测试是软件开发中重要的质量保证手段,NUnit 是一个广泛使用的 .NET 单元测试框架。本文从基础到进阶介绍了 NUnit 的使用方法,包括安装、基本用法、参数化测试、异步测试等,并探讨了常见问题和易错点,旨在帮助开发者有效利用单元测试提高代码质量和开发效率。
172 64
|
2月前
|
测试技术 API 开发者
精通.NET单元测试:MSTest、xUnit、NUnit全面解析
【10月更文挑战第15天】本文介绍了.NET生态系统中最流行的三种单元测试框架:MSTest、xUnit和NUnit。通过示例代码展示了每种框架的基本用法和特点,帮助开发者根据项目需求和个人偏好选择合适的测试工具。
46 3
|
4月前
|
测试技术 API 开发者
.NET单元测试框架大比拼:MSTest、xUnit与NUnit的实战较量与选择指南
【8月更文挑战第28天】单元测试是软件开发中不可或缺的一环,它能够确保代码的质量和稳定性。在.NET生态系统中,MSTest、xUnit和NUnit是最为流行的单元测试框架。本文将对这三种测试框架进行全面解析,并通过示例代码展示它们的基本用法和特点。
434 8
|
4月前
|
API 开发者 Java
API 版本控制不再难!Spring 框架带你玩转多样化的版本管理策略,轻松应对升级挑战!
【8月更文挑战第31天】在开发RESTful服务时,为解决向后兼容性问题,常需进行API版本控制。本文以Spring框架为例,探讨四种版本控制策略:URL版本控制、请求头版本控制、查询参数版本控制及媒体类型版本控制,并提供示例代码。此外,还介绍了通过自定义注解与过滤器实现更灵活的版本控制方案,帮助开发者根据项目需求选择最适合的方法,确保API演化的管理和客户端使用的稳定与兼容。
216 0
|
5月前
|
开发框架 前端开发 JavaScript
ABP框架测试信息---Winform端、动态网站、Vue&Element管理后端等
ABP框架测试信息---Winform端、动态网站、Vue&Element管理后端等
|
5月前
|
开发框架 JSON 前端开发
基于ABP框架的SignalR,使用Winform程序进行功能测试
基于ABP框架的SignalR,使用Winform程序进行功能测试
|
6月前
|
测试技术 C# 容器
在C#中进行单元测试 _
前言 时隔多个月,终于抽空学习了点新知识,那么这次来记录一下C#怎么进行单元测试,单元测试是做什么的。 我相信大部分刚毕业的都很疑惑单元测试是干什么的?在小厂实习了6个月后,我发现每天除了写CRUD就是写CRUD,几乎用不到单元测试。写完一个功能直接上手去测,当然这只是我个人感受,仅供参考。 然后当我还在抱怨测试好烦的时候,大佬跟我说为什么不用单元测试和集成测试,我这也是有苦说不出。要知道光学会理论知识,没有实践作为基础,都是扯淡,入职这么久还真没用过单元测试,吓得我赶紧去找资料学习。 那么也是通过观看B站某位Up主的视频,然后有点想法写下这篇文章,虽然up主的主题是探究接口的作用和意义,但是
|
存储 Java 测试技术
【C#编程最佳实践 一】单元测试实践
【C#编程最佳实践 一】单元测试实践
122 0