Skip to content

.NET & Xunit 多层级关系的单元测试

L edited this page Mar 9, 2023 · 4 revisions

我们的代码并不是一个方法可以完成的,我们通常会设置很多层级,比如ControllerApplicationServiceDomainServiceRepository,这个时候,单元测试应该怎么写呢?我们应该模拟下层方法(接口)的返回结果,有且只测试当前方法的代码逻辑。
下面以NSubstitute为例,我们的代码有TeacherManagerTeacherService两层,TeacherService会调用TeacherManager的一些方法。
分别为它们写单元测试,TeacherServiceUnitTestTeacherManagerUnitTest
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

Clone this wiki locally