将测试分为多个测试的经验法则是什么?

时间:2019-01-27 16:28:12

标签: unit-testing testing automated-tests

好的,所以我有这段代码:

public TbMtUserDTO RecoverUser(long userId, UpdateTbMtUserDTO updatedData)
{
    TbMtUser user = _usersRepository.FindUserById(userId);
    if (user == null || 
       (updatedData.IdRecSet == "Password" && String.IsNullOrEmpty(updatedData.DsUpdatedPassword)))
    {
        return null;
    }

    switch (updatedData.IdRecSet)
    {
        case "Username":
            return _mapper.Map<TbMtUserDTO>(user);
        case "Password":
            user.DsPassword = PasswordHasher.Hash(updatedData.DsUpdatedPassword);
            _usersRepository.SaveChanges();
            return _mapper.Map<TbMtUserDTO>(user);
    }
    throw new InvalidOperationException(
        String.Format(RECOVER_USER_ERROR, updatedData.IdRecSet));
}

当我为那段代码编写测试用例时,当我不得不为其中一种“密码”用例编写测试时,这就是我所做的:

    [Fact]
    public void UpdatesPasswordSuccessfully()
    {
        string oldPassword = _user.DsPassword;
        UpdateTbMtUserDTO updateTbMtUserDto = new UpdateTbMtUserDTO()
        {
            IdRecSet = "Password",
            DsUpdatedPassword = "new_password"
        };
        _usersRepositoryMock
            .Setup(x => x.FindUserById(It.IsAny<long>()))
            .Returns(_user);
        _mapperMock
            .Setup(x => x.Map<TbMtUserDTO>(It.IsAny<TbMtUser>()))
            .Returns(new TbMtUserDTO());
        TbMtUserDTO userDto = _usersService.RecoverUser(_user.CdUser, updateTbMtUserDto);
        _usersRepositoryMock.Verify(x => x.SaveChanges(), Times.Once);
        Assert.NotNull(userDto);
        Assert.True(oldPassword != _user.DsPassword);
    }

如您所见,该测试的底部有三个断言。我首先检查是否调用了SaveChanges,然后验证该方法实际上返回了什么,因此返回了NotNull断言,并且它实际上修改了密码(True断言)。

但是我觉得这不是正确的方法。但是在我看来,这些测试是相关的,但是我不确定是否应该将它们分为三个不同的测试。关键是我必须为这三种情况安排相同的零件,老实说,我也不认为这是个好主意。

你们怎么看?我已经进行了几个月的单元测试,所以在这种情况下您的经验法则是什么?

2 个答案:

答案 0 :(得分:1)

也许如果您考虑将一个测试拆分为多个测试,则应该将您的方法拆分为多个类/方法并为它们编写测试?我不想深入研究体系结构,但这可以是一个解决方案。特别是我会分开:

if (user == null || (updatedData.IdRecSet == "Password" 
        && String.IsNullOrEmpty(updatedData.DsUpdatedPassword)))
{
    return null;
}

还有这个

user.DsPassword = PasswordHasher.Hash(updatedData.DsUpdatedPassword);
_usersRepository.SaveChanges();

答案 1 :(得分:0)

有一个规则,即每个测试应测试一个特定方面。但是,这就留下了一个问题,是什么构成一个方面?对我来说,一个经验法则是,如果SUT中发生可能仅影响两个断言之一的可能变化,则两个断言代表两个不同的方面。

举个例子:假设在某些游戏中,您总是从具有定义的3D坐标的特定空间位置(可能是空间站)开始。要测试初始化​​功能,请检查该初始坐标是否具有期望值。这三个值共同构成一个方面:如果您在某个时间点决定游戏应该在不同的地方开始,则所有三个坐标都将立即更改(嗯,理论上并不需要全部更改,但这很奇怪。巧合)。

在您的示例中,以下事实使情况变得更加复杂:您的函数执行了多项操作,并且将null的返回用于不同的目的。更具体地说,根据参数的内容,该函数只是执行查找(用户名),或另外进行一些更改(密码)。因此,这不仅是拆分测试的问题,而且可能还在于拆分功能。

我可以想象将其分为两部分:一个执行查询的功能:

TbMtUser user = _usersRepository.FindUserById(userId);
if (user != null) {
    return _mapper.Map<TbMtUserDTO>(user);
} else {
    return null;
}

第二个更改已查找用户的密码-在您的情况下,这可能并不简单,因为内部使用的类型为TbMtUser,而返回的类型为TbMtUserDTO,并且我不清楚它们之间的关系...