将测试拆分为多个测试有哪些经验法则?



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

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断言)。

但我有点觉得这不是正确的方法。但在我的脑海中,这些测试是相关的,但我不确定我是否应该将它们分成三个不同的测试。问题是我必须为这三个案例安排相同的部分,说实话,我认为这也不是一个好主意。

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

也许如果您考虑将测试拆分为多个测试,您应该将方法拆分为多个类/方法并为它们编写测试?我不想深入建筑,但这可能是一个解决方案。特别是我会分开这个:

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

而这个

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

有一条规则,每个测试都应该测试一个特定的方面。 然而,这留下了一个悬而未决的问题,什么是一个方面? 对我来说,一个经验法则是,如果 SUT 中有一个合理的变化可能只影响两个断言中的一个,那么两个断言代表两个不同的方面。

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

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

我可以想象将其分为两部分:一个执行查找的函数:

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

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

最新更新