1、介绍数据库
在本文中,我将介绍如何为基于ASP.NET Boilerplate的项目建立单元测试。 我将使用本文开发的相同的应用程序(使用AngularJs,ASP.NET MVC,Web API和EntityFramework来构建NLayered单页面Web应用程序)而不是建立要测试的新应用程序。 解决方案结构就是这样:架构
咱们将测试项目的应用服务。 它包括SimpleTaskSystem.Core,SimpleTaskSystem.Application和SimpleTaskSystem.EntityFramework项目。 您能够阅读本文,了解如何构建此应用程序。 在这里,我将专一于测试。框架
参照项目:http://pan.baidu.com/s/1gf9xEU3异步
2、建立一个项目async
我建立了一个名为SimpleTaskSystem.Test的新类库项目,并添加了如下nuget包:ide
当咱们添加这些包时,它们的依赖关系也将被自动添加。 最后,咱们应该添加对SimpleTaskSystem.Application,SimpleTaskSystem.Core和SimpleTaskSystem.EntityFramework程序集的引用,由于咱们将测试这些项目。函数
2、准备一个基础测试类单元测试
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。所以,测试之间是相互隔离的。ui
在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 { }
此模块仅定义依赖模块,将进行测试。
3、建立第一个测试
咱们将建立第一个单元测试来测试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模式,包括三个步骤:
在Should_Create_New_Tasks方法中,咱们将建立两个任务,一个将被分配给Thomas More。 因此,咱们的三个步骤是:
在这里,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
我认为第二个更容易和天然地写和阅读。 应该有不少其余的扩展方法,使咱们的生活更轻松。 看到它的文档。
4、测试异常
我想为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基础架构完成的。
5、在测试中使用存储库
我将测试从一我的到另外一我的分配一个任务:
//试图将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。 您可使用这些方法之一或混合。
6、测试异步方法
咱们也可使用xUnit来测试异步方法。 请参阅写入以测试PersonAppService的GetAllPeople方法的方法。 GetAllPeople方法是异步的,因此测试方法也应该是异步的:
[Fact] public async Task Should_Get_All_People() { var output = await _personAppService.GetAllPeople(); output.People.Count.ShouldBe(4); }
7、概要
在本文中,我想显示简单的测试项目开发ASP.NET Boilerplate应用程序框架。 ASP.NET Boilerplate为实现测试驱动开发提供了良好的基础设施,或者简单地为您的应用程序建立了一些单元/集成测试。
Effort库提供了一个与EntityFramework工做良好的假数据库。 只要您使用EntityFramework和LINQ执行数据库操做,它就能够工做。 若是你有一个存储过程而且要测试它,Effort不工做。 对于这种状况,我建议使用LocalDB。