[我意识到这个问题似乎是诸如this,this,this,this和this之类的问题的重复。但是,我要特别问的是,如何使用底特律风格对具有多个代码路径的非平凡代码编写单元测试。其他问题,文章和移植都讨论了琐碎的示例,例如Calculator
类。此外,我正在练习CQS或命令查询分离,它会改变编写测试的方法。
[根据马丁·福勒(Martin Fowler)的文章“ Mocks Aren't Stubs”,我了解对TDD有两种流派-古典派(底特律)和莫克提斯特(伦敦)。
[一般来说,当我第一次学习单元测试和TDD时,我就使用Java的Mockito之类的Mocking框架来学习伦敦风格。我不知道古典TDD的存在。
伦敦式Mocks的过度利用使我感到担忧,因为测试与实现紧密相关,使它们变得脆弱。考虑到我编写的许多测试本质上都是使用模拟的行为,因此我想学习并了解您如何使用古典风格编写测试。
为此,我有几个问题。对于经典测试,
为了进一步说明,这是一个用于在REST API中注册用户的非平凡的真实代码示例。
public async signUpUser(userDTO: CreateUserDTO): Promise<void> {
const validationResult = this.dataValidator.validate(UserValidators.createUser, userDTO);
if (validationResult.isLeft())
return Promise.reject(CommonErrors.ValidationError.create('User', validationResult.value));
const [usernameTaken, emailTaken] = await Promise.all([
this.userRepository.existsByUsername(userDTO.username),
this.userRepository.existsByEmail(userDTO.email)
]) as [boolean, boolean];
if (usernameTaken)
return Promise.reject(CreateUserErrors.UsernameTakenError.create());
if (emailTaken)
return Promise.reject(CreateUserErrors.EmailTakenError.create());
const hash = await this.authService.hashPassword(userDTO.password);
const user: User = { id: 'create-an-id', ...userDTO, password: hash };
await this.userRepository.addUser(user);
this.emitter.emit('user-signed-up', user);
}
[据我所知,我通常会在这里模拟每个依赖关系,让模拟对给定参数响应某些结果,然后断言已使用正确的用户调用了存储库addUser
方法。
[使用经典的测试方法,我有一个FakeUserRepository
,它对内存中的集合进行操作并声明存储库的状态。问题是,我不确定dataValidator
和authService
如何适合。它们是否应该是真正实现数据验证和哈希密码的真实实现?或者,它们是否也应该是假冒伪劣产品,以兑现各自的接口并向某些输入返回预先编程的响应?
在其他Service方法中,有一个异常处理程序,该处理程序基于从authService
引发的异常来引发某些异常。在这种情况下,您如何进行基于状态的测试?您是否需要构建一个Fake来兑现接口并根据某些输入抛出异常?如果是这样,我们现在基本上不回到创建模拟了吗?
[为您提供不确定类型的函数的另一个示例,请参阅此JWT令牌解码方法,该方法是我的AuthenticationService
的一部分:
public verifyAndDecodeAuthToken(
candidateToken: string,
opts?: ITokenDecodingOptions
): Either<AuthorizationErrors.AuthorizationError, ITokenPayload> {
try {
return right(
this.tokenHandler.verifyAndDecodeToken(candidateToken, 'my-secret', opts) as ITokenPayload
);
} catch (e) {
switch (true) {
case e instanceof TokenErrors.CouldNotDecodeTokenError:
throw ApplicationErrors.UnexpectedError.create();
case e instanceof TokenErrors.TokenExpiredError:
return left(AuthorizationErrors.AuthorizationError.create());
default:
throw ApplicationErrors.UnexpectedError.create();
}
}
}
在这里,您可以看到该函数会引发不同的错误,这对于API调用者将具有不同的含义。如果我在这里构建伪造品,我唯一能想到的就是让伪造品对硬编码输入做出某些错误响应,但是再次,这就像现在重新构建模拟框架一样。
因此,基本上,到最后,我不确定如何使用基于状态的经典断言方法编写没有模拟的单元测试,对于上面的代码示例,我希望您能提供建议, 。谢谢。
您应该使用给定依赖性或伪类的真实实现吗?
如您自己的经验所显示,对模拟的过度利用会使测试变得脆弱。因此,仅应在有理由的情况下使用模拟(或其他类型的测试双打)。使用测试双打的充分理由是:
hashPassword
使用某些加密硬件来计算哈希,但是该硬件(或正确的硬件版本)在执行单元测试的所有主机上均不可用。例如,您(通常)不模拟标准库数学函数,例如sin
或cos
,因为它们没有上述任何问题。