zoukankan      html  css  js  c++  java
  • ABP单元测试

    一、介绍

    在本文中,我将介绍如何为基于ASP.NET Boilerplate的项目创建单元测试。 我将使用本文开发的相同的应用程序(使用AngularJs,ASP.NET MVC,Web API和EntityFramework来构建NLayered单页面Web应用程序)而不是创建要测试的新应用程序。 解决方案结构就是这样:

    我们将测试项目的应用服务。 它包括SimpleTaskSystem.Core,SimpleTaskSystem.Application和SimpleTaskSystem.EntityFramework项目。 您可以阅读本文,了解如何构建此应用程序。 在这里,我将专注于测试。

    参照项目:http://pan.baidu.com/s/1gf9xEU3

    二、创建一个项目

    我创建了一个名为SimpleTaskSystem.Test的新类库项目,并添加了以下nuget包:

    • Abp.TestBase: 提供一些基类,使基于ABP的项目更容易测试。
    • Abp.EntityFramework: 我们使用EntityFramework 6.x作为ORM。
    • Effort.EF6: 可以为易于使用的EF创建一个假的,内存中的数据库。
    • xunit: 我们将使用的测试框架。 另外,添加了xunit.runner.visualstudio包以在Visual Studio中运行测试。 当我写这篇文章时,这个包是预先释放的。 所以,我在nuget包管理器对话框中选择了'Include Prerelease'。
    • Shouldly: 此库容易编写断言。
    • xunit.runner.visualstudio: 不安装此库,发现不了测试方法

    当我们添加这些包时,它们的依赖关系也将被自动添加。 最后,我们应该添加对SimpleTaskSystem.Application,SimpleTaskSystem.Core和SimpleTaskSystem.EntityFramework程序集的引用,因为我们将测试这些项目。

    二、准备一个基础测试类

    1,为了更容易地创建测试类,我将创建一个准备假数据库连接的基类:

    /// <summary>
        /// 这是所有测试类的基础类。
        /// 它准备了ABP系统,模块和一个伪造的内存数据库。
        /// 具有初始数据的种子数据库(<see cref =“SimpleTaskSystemInitialDataBuilder”/>)。
        /// 提供使用DbContext轻松使用的方法。
        /// </summary>
        public abstract class SimpleTaskSystemTestBase : AbpIntegratedTestBase<SimpleTaskSystemTestModule>
        {
            protected SimpleTaskSystemTestBase()
            {
                //种子初始数据
                UsingDbContext(context => new SimpleTaskSystemInitialDataBuilder().Build(context));
            }
    
            protected override void PreInitialize()
            {
                //假DbConnection使用Effort!
                LocalIocManager.IocContainer.Register(
                    Component.For<DbConnection>()
                        .UsingFactoryMethod(Effort.DbConnectionFactory.CreateTransient)
                        .LifestyleSingleton()
                    );
    
                base.PreInitialize();
            }
    
            public void UsingDbContext(Action<SimpleTaskSystemDbContext> action)
            {
                using (var context = LocalIocManager.Resolve<SimpleTaskSystemDbContext>())
                {
                    context.DisableAllFilters();
                    action(context);
                    context.SaveChanges();
                }
            }
    
            public T UsingDbContext<T>(Func<SimpleTaskSystemDbContext, T> func)
            {
                T result;
    
                using (var context = LocalIocManager.Resolve<SimpleTaskSystemDbContext>())
                {
                    context.DisableAllFilters();
                    result = func(context);
                    context.SaveChanges();
                }
    
                return result;
            }
        }

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

    在SimpleTaskSystemTestBase的PreInitialize方法中,我们正在使用Effort注册DbConnection到依赖注入系统(PreInitialize方法用于运行一些代码,仅用于ABP初始化)。 我们将其注册为Singleton(用于LocalIocConainer)。 因此,即使我们在同一测试中创建了多个DbContext,测试中也将使用相同的数据库(和连接)。

    SimpleTaskSystemTestBase的UsingDbContext方法使得当我们需要直接使用DbContect来处理数据库时,可以更容易地创建DbContextes。 在构造函数中,我们使用它。 另外,我们将在测试中看到如何使用它。

    SimpleTaskSystemDbContext必须具有获取DbConnection的构造函数才能使用该内存数据库。 所以,我添加下面的构造函数接受一个DbConnection:

    public class SimpleTaskSystemDbContext : AbpDbContext
    {
        public virtual IDbSet<Task> Tasks { get; set; }
        public virtual IDbSet<Person> People { get; set; }
    
        public SimpleTaskSystemDbContext()
            : base("Default")
        {
    
        }
    
        public SimpleTaskSystemDbContext(string nameOrConnectionString)
            : base(nameOrConnectionString)
        {
                
        }
    
        //这个构造函数用于测试
        public SimpleTaskSystemDbContext(DbConnection connection)
            : base(connection, true)
        {
    
        }
    }

    在SimpleTaskSystemTestBase的构造函数中,我们还在数据库中创建一个初始数据。 这很重要,因为一些测试需要数据库中存在的数据。 SimpleTaskSystemInitialDataBuilder类填充数据库,如下所示:

    public class SimpleTaskSystemInitialDataBuilder
    {
        public void Build(SimpleTaskSystemDbContext context)
        {
            //添加一些人     
            context.People.AddOrUpdate(
                p => p.Name,
                new Person {Name = "Isaac Asimov"},
                new Person {Name = "Thomas More"},
                new Person {Name = "George Orwell"},
                new Person {Name = "Douglas Adams"}
                );
            context.SaveChanges();
    
            //添加一些任务
            context.Tasks.AddOrUpdate(
                t => t.Description,
                new Task
                {
                    Description = "my initial task 1"
                },
                new Task
                {
                    Description = "my initial task 2",
                    State = TaskState.Completed
                },
                new Task
                {
                    Description = "my initial task 3",
                    AssignedPerson = context.People.Single(p => p.Name == "Douglas Adams")
                },
                new Task
                {
                    Description = "my initial task 4",
                    AssignedPerson = context.People.Single(p => p.Name == "Isaac Asimov"),
                    State = TaskState.Completed
                });
            context.SaveChanges();
        }
    }

    我们所有的测试类都将从SimpleTaskSystemTestBase继承。 因此,所有测试都将通过使用具有初始数据的假数据库初始化ABP来启动。 我们还可以为此基类添加常用的帮助方法,以便使测试更容易。

    2,我们应该创建一个专门用于测试的模块。 这是SimpleTaskSystemTestModule在这里:

    [DependsOn(
            typeof(SimpleTaskSystemDataModule),
            typeof(SimpleTaskSystemApplicationModule)
        )]
    public class SimpleTaskSystemTestModule : AbpModule
    {
            
    }

    此模块仅定义依赖模块,将进行测试。

    三、创建第一个测试

    我们将创建第一个单元测试来测试TaskAppService类的CreateTask方法。

    TaskAppService类和CreateTask方法定义如下:

    public class TaskAppService : ApplicationService, ITaskAppService
    {
        private readonly ITaskRepository _taskRepository;
        private readonly IRepository<Person> _personRepository;
            
        public TaskAppService(ITaskRepository taskRepository, IRepository<Person> personRepository)
        {
            _taskRepository = taskRepository;
            _personRepository = personRepository;
        }
            
        public void CreateTask(CreateTaskInput input)
        {
            Logger.Info("Creating a task for input: " + input);
    
            var task = new Task { Description = input.Description };
    
            if (input.AssignedPersonId.HasValue)
            {
                task.AssignedPerson = _personRepository.Load(input.AssignedPersonId.Value);
            }
    
            _taskRepository.Insert(task);
        }
    
        //...other methods
    }

    我们先创建一个测试来测试CreateTask方法。

     public class TaskAppService_Tests : SimpleTaskSystemTestBase
        {
            private readonly ITaskAppService _taskAppService;
    
            public TaskAppService_Tests()
            {
                //创建被测试的类(SUT(Software Under Test) - 被测系统)
                _taskAppService = LocalIocManager.Resolve<ITaskAppService>();
            }
            [Fact]
            public void Should_Create_New_Tasks()
            {
                //准备测试
                var initialTaskCount = UsingDbContext(context => context.Tasks.Count());
                var thomasMore = GetPerson("Thomas More");
    
                //运行SUT
                _taskAppService.CreateTask(
                    new CreateTaskInput
                    {
                        Description = "my test task 1"
                    });
                _taskAppService.CreateTask(
                    new CreateTaskInput
                    {
                        Description = "my test task 2",
                        AssignedPersonId = thomasMore.Id
                    });
    
                //检查结果
                UsingDbContext(context =>
                {
                    context.Tasks.Count().ShouldBe(initialTaskCount + 2);
                    context.Tasks.FirstOrDefault(t => t.AssignedPersonId == null && t.Description == "my test task 1").ShouldNotBe(null);
                    var task2 = context.Tasks.FirstOrDefault(t => t.Description == "my test task 2");
                    task2.ShouldNotBe(null);
                    task2.AssignedPersonId.ShouldBe(thomasMore.Id);
                });
            }
    private Person GetPerson(string name)
            {
                return UsingDbContext(context => context.People.Single(p => p.Name == name));
            }
        }

    如前所述,我们从SimpleTaskSystemTestBase继承。 在单元测试中,我们应该创建要测试的对象。 在构造函数中,我使用LocalIocManager(依赖注入管理器)来创建一个ITaskAppService(它创建了TaskAppService,因为它实现了ITaskAppService)。 以这种方式,我摆脱了创建依赖关系的模拟实现。

    Should_Create_New_Tasks是测试方法。 它使用xUnit的Fact属性进行装饰。 因此,xUnit了解这是一种测试方法,它运行该方法。

    在测试方法中,我们通常遵循AAA模式,包括三个步骤:

    1. Arrange(安排): 准备测试
    2. Act(行为): 运行SUT(被测软件 - 实际测试代码)
    3. Assert(断言): 检查并验证结果

    在Should_Create_New_Tasks方法中,我们将创建两个任务,一个将被分配给Thomas More。 所以,我们的三个步骤是:

    1. Arrange: 我们从数据库获取该人(Thomas More),以获取数据库中的Id和当前任务数量(另外,我们在构造函数中创建了TaskAppService)。
    2. Act: 我们正在使用TaskAppService.CreateTask方法创建两个任务。
    3. Assert: 我们正在检查任务计数是否增加2.我们还尝试从数据库获取创建的任务,以查看它们是否正确插入数据库。

    在这里,UsingDbContext方法可以帮助我们直接使用DbContext。 如果此测试成功,我们了解如果我们提供有效的输入,CreateTask方法可以创建任务。 此外,存储库正在工作,因为它将Tasks插入数据库。

    要运行测试,我们通过选择TEST Windows Test Explorer打开Visual Studio测试资源管理器:

     

    然后我们点击测试资源管理器中的“全部运行”链接。 它在解决方案中找到并运行所有测试:

    如上所示,我们的第一个单元测试通过。恭喜! 如果测试或测试代码不正确,测试将失败。 假设我们已经忘记将赋值赋给给某人(要测试它,注释掉TaskAppService.CreateTask方法中的相关行)。 当我们运行测试时,它将失败:

    Shouldly库使得失败的消息更清晰。 它也使写入断言变得容易。 比较xUnit的Assert.Equal与Shouldly的ShouldBe扩展方法:

    Assert.Equal(thomasMore.Id, task2.AssignedPersonId); //Using xunit's Assert
    task2.AssignedPersonId.ShouldBe(thomasMore.Id); //Using Shouldly

    我认为第二个更容易和自然地写和阅读。 应该有很多其他的扩展方法,使我们的生活更轻松。 看到它的文档。

    四、测试异常

    我想为CreateTask方法创建第二个测试。 但是,这次输入无效:

    [Fact]
    public void Should_Not_Create_Task_Without_Description()
    {
        //说明未设置
        Assert.Throws<AbpValidationException>(() => _taskAppService.CreateTask(new CreateTaskInput()));
    }

    我希望CreateTask方法抛出AbpValidationException,如果我没有设置描述创建任务。 因为在CreateTaskInput DTO类中将描述属性标记为必需(请参阅源代码)。 如果CreateTask引发异常,则此测试成功,否则失败。 注意; 验证输入和抛出异常是由ASP.NET Boilerplate基础架构完成的。

    五、在测试中使用存储库

    我将测试从一个人到另一个人分配一个任务:

            //试图将Isaac Asimov的任务分配给Thomas More
            [Fact]
            public void Should_Change_Assigned_People()
            {
                //我们可以使用存储库而不是DbContext
                var taskRepository = LocalIocManager.Resolve<ITaskRepository>();
    
                //获取测试数据
                var isaacAsimov = GetPerson("Isaac Asimov");
                var thomasMore = GetPerson("Thomas More");
                var targetTask = taskRepository.FirstOrDefault(t => t.AssignedPersonId == isaacAsimov.Id);
                targetTask.ShouldNotBe(null);
    
                //运行 SUT
                _taskAppService.UpdateTask(
                    new UpdateTaskInput
                    {
                        TaskId = targetTask.Id,
                        AssignedPersonId = thomasMore.Id
                    });
    
                //检查结果
                taskRepository.Get(targetTask.Id).AssignedPersonId.ShouldBe(thomasMore.Id);
            }

    在这个测试中,我使用ITaskRepository执行数据库操作,而不是直接使用DbContext。 您可以使用这些方法之一或混合。

    六、测试异步方法

    我们也可以使用xUnit来测试异步方法。 请参阅写入以测试PersonAppService的GetAllPeople方法的方法。 GetAllPeople方法是异步的,所以测试方法也应该是异步的:

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

    七、概要

    在本文中,我想显示简单的测试项目开发ASP.NET Boilerplate应用程序框架。 ASP.NET Boilerplate为实现测试驱动开发提供了良好的基础设施,或者简单地为您的应用程序创建了一些单元/集成测试。

    Effort库提供了一个与EntityFramework工作良好的假数据库。 只要您使用EntityFramework和LINQ执行数据库操作,它就可以工作。 如果你有一个存储过程并且要测试它,Effort不工作。 对于这种情况,我建议使用LocalDB。

  • 相关阅读:
    CQUOJ 10819 MUH and House of Cards
    CQUOJ 9920 Ladder
    CQUOJ 9906 Little Girl and Maximum XOR
    CQUOJ 10672 Kolya and Tandem Repeat
    CQUOJ 9711 Primes on Interval
    指针试水
    Another test
    Test
    二分图匹配的重要概念以及匈牙利算法
    二分图最大匹配
  • 原文地址:https://www.cnblogs.com/zd1994/p/7705069.html
Copyright © 2011-2022 走看看