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。

  • 相关阅读:
    Symbol Commands
    Control Structures
    script.stub
    Lowest Common Ancestor of a Binary Search Tree
    Move Zeroes
    Odd Even Linked List
    ubuntu18.04系统安装及php7.2,apache2,mysql8,git,svn,composer,vs code 到安装 php 扩展配置php.ini 实现 laravel5.8 运行
    thinkphp3.2.3 自定义路由实践
    thinkphp3.2.3 自动验证 unique 出错的解决办法
    【重磅】中国集成电路产业基金投资版图详解
  • 原文地址:https://www.cnblogs.com/zd1994/p/7705069.html
Copyright © 2011-2022 走看看