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 + );
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模式,包括三个步骤:
- Arrange(安排): 准备测试
- Act(行为): 运行SUT(被测软件 - 实际测试代码)
- Assert(断言): 检查并验证结果
在Should_Create_New_Tasks方法中,我们将创建两个任务,一个将被分配给Thomas More。 所以,我们的三个步骤是:
- Arrange: 我们从数据库获取该人(Thomas More),以获取数据库中的Id和当前任务数量(另外,我们在构造函数中创建了TaskAppService)。
- Act: 我们正在使用TaskAppService.CreateTask方法创建两个任务。
- 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();
}
七、概要
在本文中,我想显示简单的测试项目开发ASP.NET Boilerplate应用程序框架。 ASP.NET Boilerplate为实现测试驱动开发提供了良好的基础设施,或者简单地为您的应用程序创建了一些单元/集成测试。
Effort库提供了一个与EntityFramework工作良好的假数据库。 只要您使用EntityFramework和LINQ执行数据库操作,它就可以工作。 如果你有一个存储过程并且要测试它,Effort不工作。 对于这种情况,我建议使用LocalDB。
ABP单元测试的更多相关文章
- 使用xUnit,EF,Effort和ABP进行单元测试(C#)
返回总目录<一步一步使用ABP框架搭建正式项目系列教程> 本篇目录 介绍 创建测试项目 准备测试基类 创建第一个测试 测试异常 在测试中使用仓储 测试异步方法 小结 介绍 在这篇博客中,我 ...
- ABP中单元测试的技巧:Mock和数据驱动
(此文章同时发表在本人微信公众号"dotNET每日精华文章",欢迎右边二维码来关注.) 题记:虽然ABP为大家提供了测试的脚手架了,不过有些小技巧还是需要自己探索的. ASP.NE ...
- ABP入门系列(11)——编写单元测试
ABP入门系列目录--学习Abp框架之实操演练 源码路径:Github-LearningMpaAbp 1. 前言 In computer programming, unit testing is a ...
- 在 ABP vNext 中编写仓储单元测试的问题一则
一.问题 新项目是基于 ABP vNext 框架进行开发的,所以我要求为每层编写单元测试.在同事为某个仓储编写单元测试的时候,发现了一个奇怪的问题.他的对某个聚合根的 A 字段进行了更新,随后对某个导 ...
- [Abp vNext 源码分析] - 18. 单元测试
简介 ABP vNext 框架使用 xUnit 作为单元测试组件,官方的所有模块都编写了大量的 单元/集成测试 确保功能正常.由于 ABP vNext 模块化系统的原因,开发人员在建立单元测试项目的时 ...
- ABP Xunit单元测试 第五篇
1.创建如下的项目结构 public class TestName { public bool ValidateName(string Name) { if (Name == "yin&qu ...
- 基于DDD的现代ASP.NET开发框架--ABP系列文章总目录
ABP相关岗位招聘:给热爱.NET新技术和ABP框架的朋友带来一个高薪的工作机会 ABP交流会录像视频:ABP架构设计交流群-7月18日上海线下交流会的内容分享(有高清录像视频的链接) 代码自动生成: ...
- 一步一步使用ABP框架搭建正式项目系列教程
研究ABP框架好多天了,第一次看到这个框架的名称到现在已经很久了,但由于当时内功有限,看不太懂,所以就只是大概记住了ABP这个名字.最近几天,看到了园友@阳光铭睿的系列ABP教程,又点燃了我内心要研究 ...
- ABP文档 - 本地化
文档目录 本节内容: 简介 应用语言 本地化源 XML文件 注册XML本地化源 JSOn文件 注册JSON本地化源 资源文件 自定义源 获取一个本地文本 在服务端 在MVc控制器里 在MVC视图里 在 ...
随机推荐
- 开源.NET界面库
一.十大开源的.NET用户界面框架 选择一款合适的GUI框架是.NET开发中比较重要但又很棘手的问题,因为用户界面相当于一款应用的"门面",直接面向用户.好的UI更能吸引用户,有时 ...
- java基础基础总结----- RunTime
- os.chmod()--更改目录授权权限
用法:os.chmod() 方法用于更改文件或目录的权限. 语法:os.chmod(path, mode) 参数:只需要2个参数,一个是路径,一个是说明路径的模式. path -- 文件名路径或目录路 ...
- python的内置模块re模块方法详解以及使用
正则表达式 一.普通字符 . 通配符一个.只匹配一个字符 匹配任意除换行符"\n"外的字符(在DOTALL模式中也能匹配换行符 >>> import re ...
- Hadoop生态圈-Kafka的新API实现生产者-消费者
Hadoop生态圈-Kafka的新API实现生产者-消费者 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任.
- Java基础-算术运算符(Arithmetic Operators)
Java基础-算术运算符(Arithmetic Operators) 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. Java程序通过运算符实现对数据的处理,Java中的运算符包括: ...
- Python print list列表里面的中文出错
其实也不是出错啦,是编码格式不正确 看,我要这样 student=[] ): name=raw_input('输入姓名:') student.append(name) print student 结果 ...
- mysql 不同引擎的比较
mysql 支持的默认引擎是InnoDB,其他的常用引擎包括MyISAM等,那么他们有什么差别呢. 首先执行 show engines; 来查看数据库当前支持的引擎. 可以看到mysql支持这么多不同 ...
- jQuery1.11源码分析(3)-----Sizzle源码中的浏览器兼容性检测和处理[原创]
上一章讲了正则表达式,这一章继续我们的前菜,浏览器兼容性处理. 先介绍一个简单的沙盒测试函数. /** * Support testing using an element * @param {Fun ...
- Hive笔记之严格模式(strict mode)
Hive有一个严格模式,在严格模式下会对可能产生较大查询结果的语句做限制,禁止其提交执行. 一.切换严格模式 查看当前的模式: hive> set hive.mapred.mode; hive. ...