ABP编写单元测试
首先来看下[Fact]的简单示例:
public >Class1
{
[Fact]
public void PassingTest()
{
Assert.Equal(4, Add(2, 2));
}
[Fact]
public void FailingTest()
{
Assert.Equal(5, Add(2, 2));
}
int Add(int x, int y)
{
return x + y;
}
}
其中xUnit.Net提供了三种继承于DataAttribute
的特性([InlineData]、 [ClassData]、 [PropertyData])用于为[Theory]标记的参数化测试方法传参。
下面是使用这三种特性传参的实例:
InlineData Example
public >StringTests1
{
[Theory,
InlineData("goodnight moon", "moon", true),
InlineData("hello world", "hi", false)]
public void Contains(string input, string sub, bool expected)
{
var actual = input.Contains(sub);
Assert.Equal(expected, actual);
}
}
PropertyData Example
public >StringTests2
{
[Theory, PropertyData("SplitCountData")]
public void SplitCount(string input, int expectedCount)
{
var actualCount = input.Split(' ').Count();
Assert.Equal(expectedCount, actualCount);
}
public static IEnumerable<object[]> SplitCountData
{
get
{
// Or this could read from a file. :)
return new[]
{
new object[] { "xUnit", 1 },
new object[] { "is fun", 2 },
new object[] { "to test with", 3 }
};
}
}
}
ClassData Example
public >{
[Theory, ClassData(typeof(IndexOfData))]
public void IndexOf(string input, char letter, int expected)
{
var actual = input.IndexOf(letter);
Assert.Equal(expected, actual);
}
}
public >: IEnumerable<object[]>
{
private readonly List<object[]> _data = new List<object[]>
{
new object[] { "hello world", 'w', 6 },
new object[] { "goodnight moon", 'w', -1 }
};
public IEnumerator<object[]> GetEnumerator()
{ return _data.GetEnumerator(); }
IEnumerator IEnumerable.GetEnumerator()
{ return GetEnumerator(); }
}
2.4. Shouldly(断言框架)
Shouldly提供的断言方式与传统的Assert相比更实用易懂。
对比一下就明白了:
Assert.That(contestant.Points, Is.EqualTo(1337));
//Expected 1337 but was 0
contestant.Points.ShouldBe(1337);
//contestant.Points should be 1337 but was 0
首先上写法上更清晰易懂,第二当测试失败时,提示消息也更清楚直接。
同样,想对Shouldly有更对了解,请直接访问Shouldly官方链接。
2.5. 测试基类XxxTestBase
首先来看看代码:
public abstract >LearningMpaAbpTestBase : AbpIntegratedTestBase<LearningMpaAbpTestModule>
{
private DbConnection _hostDb;
private Dictionary<int, DbConnection> _tenantDbs; //only used for db per tenant architecture
protected LearningMpaAbpTestBase()
{
//Seed initial data for host
AbpSession.TenantId = null;
UsingDbContext(context =>
{
new InitialHostDbBuilder(context).Create();
new DefaultTenantCreator(context).Create();
});
//Seed initial data for default tenant
AbpSession.TenantId = 1;
UsingDbContext(context =>
{
new TenantRoleAndUserBuilder(context, 1).Create();
});
LoginAsDefaultTenantAdmin();
UsingDbContext(context => new InitialDataBuilder().Build(context));
}
protected override void PreInitialize()
{
base.PreInitialize();
UseSingleDatabase();
//UseDatabasePerTenant();
}
private void UseSingleDatabase()
{
_hostDb = DbConnectionFactory.CreateTransient();
LocalIocManager.IocContainer.Register(
Component.For<DbConnection>()
.UsingFactoryMethod(() => _hostDb)
.Life >
从该段代码中我们可以看出该测试基类继承自AbpIntegratedTestBase<T>
。
在PreInitialize()
方法中指定了为租户创建单一数据库还是多个数据库。
_hostDb = DbConnectionFactory.CreateTransient();
是Effort提供的方法用来创建的DbConnection
(数据库连接)。然后将其使用单例的模式注册到IOC容器中,这样在测试中,所有的数据库连接都将使用Effort为我们创建的数据库连接。
在构造函数中主要做了两件事,预置了初始数据和种子数据,并以默认租户Admin登录。
至此我们对abp为我们默认创建的测试项目有了一个大概的认识。下面我们就开始实战阶段。
3. 单元测试实战
3.1. 理清要测试的方法逻辑
我们以应用服务层的TaskAppService的CreateTask方法为例,创建单元测试。先来看看该方法的代码:
public int CreateTask(CreateTaskInput input) {
//We can use Logger, it's defined in ApplicationService >.Info("Creating a task for input: " + input);
//判断用户是否有权限
if (input.AssignedPersonId.HasValue && input.AssignedPersonId.Value != AbpSession.GetUserId()) PermissionChecker.Authorize(PermissionNames.Pages_Tasks_AssignPerson);
var task = Mapper.Map < Task > (input);
int result = _taskRepository.InsertAndGetId(task);
//只有创建成功才发送邮件和通知
if (result > 0) {
task.CreationTime = Clock.Now;
if (input.AssignedPersonId.HasValue) {
task.AssignedPerson = _userRepository.Load(input.AssignedPersonId.Value);
var message = "You hava been assigned one task into your todo list.";
//TODO:需要重新配置QQ邮箱密码
//SmtpEmailSender emailSender = new SmtpEmailSender(_smtpEmialSenderConfig);
//emailSender.Send("ysjshengjie@qq.com", task.AssignedPerson.EmailAddress, "New Todo item", message);
_notificationPublisher.Publish("NewTask", new MessageNotificationData(message), null, NotificationSeverity.Info, new[] {
task.AssignedPerson.ToUserIdentifier()
});
}
}
该方法主要有三步,第一步判断权限,第二步保存数据库并返回Id,第三步发送通知。
3.2. 创建单元测试类并注入依赖
创建TaskAppSerice_Tests
类并继承自XxxTestBase类,并注入需要的依赖。
public >TaskAppService_Tests : LearningMpaAbpTestBase
{
private readonly ITaskAppService _taskAppService;
public TaskAppService_Tests()
{
_taskAppService = Resolve<TaskAppService>();
}
}
3.3. 创建单元测试方法
第一个方法我们应该测试Happy path(即测试方法的默认场景,没有异常和错误信息)。
[Fact]
public void Should_Create_New_Task_WithPermission() {
//Arrange
//LoginAsDefaultTenantAdmin();//基类的构造函数中已经以默认租户Admin登录。
var initalCount = UsingDbContext(ctx = >ctx.Tasks.Count());
var task1 = new CreateTaskInput() {
Title = "Test Task",
Description = "Test Task",
State = TaskState.Open
};
var task2 = new CreateTaskInput() {
Title = "Test Task2",
Description = "Test Task2",
State = TaskState.Open
};
//Act
int taskResult1 = _taskAppService.CreateTask(task1);
int taskResult2 = _taskAppService.CreateTask(task2);
//Assert
UsingDbContext(ctx = >{
taskResult1.ShouldBeGreaterThan(0);
taskResult2.ShouldBeGreaterThan(0);
ctx.Tasks.Count().ShouldBe(initalCount + 2);
ctx.Tasks.FirstOrDefault(t = >t.Title == "Test Task").ShouldNotBe(null);
var task = ctx.Tasks.FirstOrDefault(t = >t.Title == "Test Task2");
task.ShouldNotBe(null);
task.State.ShouldBe(TaskState.Open);
});
}
在这里啰嗦一下单元测试的AAA原则:
- Arrange:为测试做准备工作
- Act:运行实际测试的代码
- Assert:断言,校验结果
再说明一下单元测试的方法推荐命名规则:
some_result_occurs_when_doing...
回到我们这个测试方法。
Arrange阶段我们先以Admin登录(Admin具有所有权限),然后获取数据库中初始Task的数量,再准备了两条测试数据。
Act阶段,直接调用TaskAppService的CreateTask方法。
Assert阶段:首先判断CreateTask的返回值大于0 ;再判断现在数据库的数量是否增加了2条;再校验数据库中是否包含创建的Task,并核对Task的状态。
3.4. 预置数据
在进行测试的时候,我们肯定需要一些测试数据,以便我们进行合理的测试。
在基础设施层,我们有专门的SeedData目录用来预置种子数据。但是进行单元测试的测试数据不应该污染实体数据库,所以直接在SeedData目录预置数据就不太现实。
3.4.1. 创建TestDataBuilder
所以,我们就直接在测试项目中,新建一个TestDatas文件夹来管理测试种子数据。
然后创建TestDataBuilder
类,通过该类来统一创建所需的测试数据。(注意,需要修改下类中的_context类型为你自己项目对应的DbContext)
namespace LearningMpaAbp.Tests.TestDatas
{
public >TestDataBuilder
{
private readonly LearningMpaAbpDbContext _context;
private readonly int _tenantId;
public TestDataBuilder(LearningMpaAbpDbContext context, int tenantId)
{
_context = context;
_tenantId = tenantId;
}
public void Create()
{
_context.DisableAllFilters();
//new TestUserBuilder(_context,_tenantId).Create();
//new TestTasksBuilder(_context,_tenantId).Create();
_context.SaveChanges();
}
}
}
然后修改我们的测试基类XxxTestBase
,在构造函数调用我们新建的TestDataBuilder
的Create()
方法。new TestDataBuilder(context, 1).Create();
,如下图:
3.4.2. 创建Task测试数据
创建TestTasksBuilder
,如下:(注意,需要修改下类中的_context类型为你自己项目对应的DbContext)
namespace LearningMpaAbp.Tests.TestDatas
{
public >TestTasksBuilder
{
private readonly LearningMpaAbpDbContext _context;
private readonly int _tenantId;
public TestTasksBuilder(LearningMpaAbpDbContext context, int tenantId)
{
_context = context;
_tenantId = tenantId;
}
public void Create()
{
for (int i = 0; i < 8; i++)
{
var task = new Task()
{
Title = "TestTask" + i,
Description = "Test Task " + i,
CreationTime = DateTime.Now,
State = (TaskState)new Random().Next(0, 1)
};
_context.Tasks.Add(task);
}
}
}
}
然后再在TestDataBuild
中调用该类的Create()
的方法即可。
new TestTasksBuilder(_context,_tenantId).Create();
3.5. Run the test(单元测试跑起来)
喜闻乐见的绿色,单元测试通过。
3.6. 完善测试用例
单元测试中我们仅仅测试Happy Path是远远不够的。因为毕竟我们只是测试了正常的正确场景。为了提高单元测试的覆盖度,我们应该针对代码可能出现的异常问题进行测试。
还拿我们刚刚的CreateTask方法为例,其中第二步有一个验证权限操作,当用户没有权限的时候,Task应该不能创建并抛出异常。那我们就针对无权限的场景补充一个单元测试吧。
3.6.1. 预置数据
无权限简单,直接创建一个新用户登录就ok了。但为了用户复用,我们还是在种子数据中预置测试用户吧。
回到我们的TestDatas
目录,创建TestUserBuilder
,来预置测试用户。
namespace LearningMpaAbp.Tests.TestDatas
{
/// <summary>
/// 预置测试用户(无权限)
/// </summary>
public >TestUserBuilder
{
private readonly LearningMpaAbpDbContext _context;
private readonly int _tenantId;
public TestUserBuilder(LearningMpaAbpDbContext context, int tenantId)
{
_context = context;
_tenantId = tenantId;
}
public void Create()
{
var testUser =
_context.Users.FirstOrDefault(u => u.TenantId == _tenantId && u.UserName == "TestUser");
if (testUser == null)
{
testUser = new User
{
TenantId = _tenantId,
UserName = "TestUser",
Name = "Test User",
Surname = "Test",
EmailAddress = "test@defaulttenant.com",
Password = User.DefaultPassword,
IsEmailConfirmed = true,
IsActive = true
};
_context.Users.Add(testUser);
}
}
}
}
然后再在TestDataBuild
中调用该类的Create()
的方法即可。
new TestUserBuilder(_context,_tenantId).Create();
3.6.2. 完善单元测试
/// <summary>
/// 若没有分配任务给他人的权限,创建的任务指定给他人,则任务创建不成功。
/// </summary>
[Fact]
public void Should_Not_Create_New_Order_AssignToOrther_WithoutPermission()
{
//Arrange
LoginAsTenant(Tenant.DefaultTenantName, "TestUser");
//获取admin用户
var adminUser = UsingDbContext(ctx => ctx.Users.FirstOrDefault(u => u.UserName == User.AdminUserName));
var newTask = new CreateTaskInput()
{
Title = "Test Task",
Description = "Test Task",
State = TaskState.Open,
AssignedPersonId = adminUser.Id //TestUser创建Task并分配给Admin
};
//Act,Assert
Assert.Throws<AbpAuthorizationException>(() => _taskAppService.CreateTask(newTask));
}
当用户无权限时,将抛出Abp封装的AbpAuthorizationException(未授权异常)。
单元测试用例,就讲这两个,剩下的自己动手完善吧。源码中已经覆盖测试,可供参考。
4. 总结
这篇文章中主要梳理了Abp中如何进行单元测试,以及依赖的xUnit、Effort、Shouldly框架的用法。并基于以上内容的总结,进行了单元测试的实战演练。
相信看完此篇文章的总结,对你在Abp中进行单元测试,有所裨益。