对非平凡函数及其遵循CQS的依赖项进行基于非模拟,基于状态的单元测试

问题描述 投票:0回答:1

[我意识到这个问题似乎是诸如thisthisthisthisthis之类的问题的重复。但是,我要特别问的是,如何使用底特律风格对具有多个代码路径的非平凡代码编写单元测试。其他问题,文章和移植都讨论了琐碎的示例,例如Calculator类。此外,我正在练习CQS或命令查询分离,它会改变编写测试的方法。

[根据马丁·福勒(Martin Fowler)的文章“ Mocks Aren't Stubs”,我了解对TDD有两种流派-古典派(底特律)和莫克提斯特(伦敦)。

[一般来说,当我第一次学习单元测试和TDD时,我就使用Java的Mockito之类的Mocking框架来学习伦敦风格。我不知道古典TDD的存在。

伦敦式Mocks的过度利用使我感到担忧,因为测试与实现紧密相关,使它们变得脆弱。考虑到我编写的许多测试本质上都是使用模拟的行为,因此我想学习并了解您如何使用古典风格编写测试。

为此,我有几个问题。对于经典测试,

  1. 您应该使用给定依赖性或伪类的真实实现吗?
  2. 底特律从业者对“单位”的定义是否与模拟主义者不同?

为了进一步说明,这是一个用于在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,它对内存中的集合进行操作并声明存储库的状态。问题是,我不确定dataValidatorauthService如何适合。它们是否应该是真正实现数据验证和哈希密码的真实实现?或者,它们是否也应该是假冒伪劣产品,以兑现各自的接口并向某些输入返回预先编程的响应?

在其他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调用者将具有不同的含义。如果我在这里构建伪造品,我唯一能想到的就是让伪造品对硬编码输入做出某些错误响应,但是再次,这就像现在重新构建模拟框架一样。

因此,基本上,到最后,我不确定如何使用基于状态的经典断言方法编写没有模拟的单元测试,对于上面的代码示例,我希望您能提供建议, 。谢谢。

unit-testing testing mocking tdd
1个回答
1
投票

您应该使用给定依赖性或伪类的真实实现吗?

如您自己的经验所显示,对模拟的过度利用会使测试变得脆弱。因此,仅应在有理由的情况下使用模拟(或其他类型的测试双打)。使用测试双打的充分理由是:

  • 您不能轻易使组件依赖(DOC)的行为符合测试的预期。例如,您的代码很健壮,并检查另一个组件的返回状态是否表示某些故障。要测试您的健壮性代码,您需要另一个组件来返回故障状态-但这对于实现真正的组件可能非常困难,甚至是不可能的。
  • 调用DOC是否会引起任何非专业行为(日期/时间,随机性,网络连接)?例如,如果代码的计算使用当前时间,那么使用真实的DOC(即时间模块),您将在每次测试运行中获得不同的结果。
  • 您要测试的结果是否是被测代码传递给DOC的某些数据,但是DOC没有获取该数据的API?例如,如果您的被测试代码将其结果写入控制台(在这种情况下,该控制台为DOC),但是您的测试无法查询该控制台写入的内容。
  • 实际DOC的测试设置过于复杂和/或需要大量维护(例如需要外部文件)。例如,DOC在固定路径下解析某些配置文件。并且,对于不同的测试用例,您将需要对DOC进行不同的配置,因此必须在该位置提供不同的配置文件。
  • 原始DOC为您的测试代码带来了可移植性问题。例如,如果您的函数hashPassword使用某些加密硬件来计算哈希,但是该硬件(或正确的硬件版本)在执行单元测试的所有主机上均不可用。
  • 使用原始DOC是否会导致构建/执行时间过长?
  • 是否存在使测试不可靠的DOC稳定性(成熟度)问题,或者更糟的是,甚至还没有DOC?
  • 也许DOC本身没有上述任何问题,但是具有其自身的依赖性,并且所产生的依赖性集合导致上述某些问题?

例如,您(通常)不模拟标准库数学函数,例如sincos,因为它们没有上述任何问题。

© www.soinside.com 2019 - 2024. All rights reserved.