.NET & Xunit 多层级关系的单元测试
我们的代码并不是一个方法可以完成的,我们通常会设置很多层级,比如Controller
、ApplicationService
、DomainService
、Repository
,这个时候,单元测试应该怎么写呢?我们应该模拟下层方法(接口)的返回结果,有且只测试当前方法的代码逻辑。
下面以NSubstitute
为例,我们的代码有TeacherManager
和TeacherService
两层,TeacherService会调用TeacherManager的一些方法。
分别为它们写单元测试,TeacherServiceUnitTest
和TeacherManagerUnitTest
。
TeacherServiceUnitTest只应该测试TeacherService的代码段,TeacherManagerUnitTest只测试TeacherManager的代码段。
那么TeacherService调用TeacherManager的部分怎么处理呢?使用NSubstitute
模拟一个虚假的TeacherManager对象,返回虚假的结果,实则并不会调用TeacherManager的内部方法。这样TeacherServiceUnitTest就可以专注于TeacherService这一层的逻辑了。
接下来我们看看TeacherService的单元测试是怎么实现的。
TeacherService的逻辑很简单:
public class TeacherService
{
TeacherManager _teacherManager;
public TeacherService(TeacherManager teacherManager)
{
_teacherManager = teacherManager;
}
public Teacher Insert(Teacher teacher)
{
if (teacher == null)
{
throw new DataNotExistException($"Data cannot be empty.");
}
if (string.IsNullOrEmpty(teacher.Name) || string.IsNullOrWhiteSpace(teacher.Name))
{
throw new NameIsEmptyOrWhiteSpaceException($"Name cannot be empty.");
}
if (teacher.Age < 0)
{
throw new AgeIsInvalidException($"Age must be greater than 0.");
}
return _teacherManager.Insert(teacher);
}
}
我们只需要测试Insert
方法,并且只需要测试验证Name和验证Age的逻辑,不需要关心_teacherManager.Insert(teacher)
内部的逻辑。
所以我们应该模拟return _teacherManager.Insert(teacher);
这一行,让它返回模拟的Teacher数据。
所以我们的TeacherServiceUnitTest这样写:
public class TeacherServiceUnitTest
{
public TeacherService _teacherService;
public TeacherServiceUnitTest()
{
var teacherManager = Substitute.For<TeacherManager>();
teacherManager.Insert(new Teacher());
_teacherService = new TeacherService(teacherManager);
}
[Fact]
public void Insert_A_Teacher()
{
var teacher = new Teacher()
{
Name = "test teacher",
Age = 30
};
_teacherService.Insert(teacher);
teacher.ShouldNotBeNull();
}
[Fact]
public void Insert_A_Teacher_Failed_Empty_Name()
{
var teacher = new Teacher()
{
Name = "",
Age = 30
};
Should.Throw<NameIsEmptyOrWhiteSpaceException>(() => _teacherService.Insert(teacher));
}
[Fact]
public void Insert_A_Teacher_Failed_WhiteSpace_Name()
{
var teacher = new Teacher()
{
Name = " ",
Age = 30
};
Should.Throw<NameIsEmptyOrWhiteSpaceException>(() => _teacherService.Insert(teacher));
}
[Fact]
public void Insert_A_Teacher_Failed_Without_Data()
{
Should.Throw<DataNotExistException>(() => _teacherService.Insert(null));
}
[Fact]
public void Insert_A_Teacher_Failed_Invalid_Age()
{
var teacher = new Teacher()
{
Name = "test teacher",
Age = -10
};
Should.Throw<AgeIsInvalidException>(() => _teacherService.Insert(teacher));
}
}
使用下面两行代码,模拟一个TeacherManager对象,并且保证每次调用Insert
方法,都会正常返回一个Teacher对象。
var teacherManager = Substitute.For<TeacherManager>();
teacherManager.Insert(new Teacher());
之后将测试的关注点集中在TeacherService的代码上,即:
if (teacher == null)
{
throw new DataNotExistException($"Data cannot be empty.");
}
if (string.IsNullOrEmpty(teacher.Name) || string.IsNullOrWhiteSpace(teacher.Name))
{
throw new NameIsEmptyOrWhiteSpaceException($"Name cannot be empty.");
}
if (teacher.Age < 0)
{
throw new AgeIsInvalidException($"Age must be greater than 0.");
}
return _teacherManager.Insert(teacher);
所以可以看到TeacherServiceUnitTest的多个方法在测试以上代码的各种情况。
示例代码
TeacherManagerUnitTest
TeacherServiceUnitTest
TeacherManager
TeacherService
学习技术最好的文档就是【官方文档】,没有之一。
还有学习资料【Microsoft Learn】、【CSharp Learn】、【My Note】。
如果,你认为阅读这篇博客让你有些收获,不妨点击一下右下角的【推荐】按钮。
如果,你希望更容易地发现我的新博客,不妨点击一下【关注】。