对遵循 CQS 的非平凡函数及其依赖项执行非模拟、基于状态的单元测试 [英] Performing non-mock, state-based unit testing of non-trivial functions and their dependencies that follow CQS

查看:47
本文介绍了对遵循 CQS 的非平凡函数及其依赖项执行非模拟、基于状态的单元测试的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我意识到这个问题似乎与这个这个这个这个和这个.然而,我特别问的是,您将如何使用底特律风格针对具有多个代码路径的非平凡代码编写单元测试.其他问题、文章和解释都讨论了诸如 Calculator 类之类的琐碎示例.此外,我正在练习 CQS,或命令查询分离,它改变了我编写测试的方法.

I realize that this question may seem to be a duplicate of questions such as this, this, this, this, and this. I'm specifically asking, however, how you would write unit tests using the Detroit style toward non-trivial code with multiple code paths. Other questions, articles, and explantations all discuss trivial examples such as a Calculator class. Further, I'm practicing CQS, or Command Query Separation, which alters the methods by which I write tests.

根据 Martin Fowler 的文章模拟不是存根",我知道有是 TDD 的两种思想流派 - Classical(底特律)和 Mockist(伦敦).

As per Martin Fowler's article "Mocks Aren't Stubs", I understand that there are two schools of thought toward TDD - Classical (Detroit) and Mockist (London).

当我第一次学习单元测试和 TDD 时,我学习的是伦敦风格,使用了像 Java 的 Mockito 这样的 Mocking 框架.我不知道经典 TDD 的存在.

When I first learned Unit Testing and TDD in general, I was taught the London style, utilizing Mocking Frameworks like Java's Mockito. I had no idea of the existence of Classical TDD.

伦敦风格 Mock 的过度使用让我感到担忧,因为测试与实现密切相关,使它们变得脆弱.考虑到我编写的许多测试本质上都是使用模拟的行为,我想学习和了解您如何使用 Classical 风格编写测试.

The overutilization of Mocks in the London style concerns me in that tests are very much tied to implementation, making them brittle. Considering a lot of tests I've written have been behavioral in nature utilizing mocks, I'd like to learn and understand how you'd write tests using the Classical style.

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

To this effect, I have a few questions. For Classical testing,

  1. 您应该使用给定依赖项的真实实现还是虚假类?
  2. 底特律从业者对单位"的定义是否与 Mockists 不同?

为了进一步详细说明,这里有一个重要的实际代码示例,用于在 REST API 中注册用户.

To further elaborate, here is a non-trivial real-world code example for signing up a user in a 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 方法是使用正确的用户.

With my knowledge of the mocking approach, I'd generally mock every single dependency here, have mocks respond with certain results for given arguments, and then assert that the repository addUser method was called with the correct user.

使用经典方法进行测试,我有一个 FakeUserRepository,它对内存中的集合进行操作并对存储库的状态进行断言.问题是,我不确定 dataValidatorauthService 是如何适应的.它们应该是真正验证数据并实际散列密码的真正实现吗?或者,它们是否也应该是假的,以尊重各自的界面并对某些输入返回预编程的响应?

Using the Classical approach to testing, I'd have a FakeUserRepository that operates on an in-memory collection and make assertions about the state of the Repository. The problem is, I'm not sure how dataValidator and authService fits in. Should they be real implementations that actually validate data and actually hash passwords? Or, should they be fakes too that honor their respective interfaces and return pre-programmed responses to certain inputs?

在其他 Service 方法中,有一个异常处理程序根据从 authService 抛出的异常抛出某些异常.在这种情况下,您如何进行基于状态的测试?您是否需要构建一个尊重界面并根据某些输入抛出异常的 Fake?如果是这样,我们现在不是基本上回到创建模拟了吗?

In other Service methods, there is an exception handler that throws certain exceptions based on exceptions thrown from the authService. How do you do state-based testing in that case? Do you need to build a Fake that honors the interface and that throws exceptions based on certain inputs? If so, aren't we basically back to creating mocks now?

再给你一个我不确定如何构建假函数的例子,请看这个 JWT 令牌解码方法,它是我的 AuthenticationService 的一部分:

To give you another example of the kind of function I'd be unsure how to build a fake for, see this JWT Token decoding method which is a part of my 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 调用者具有不同的含义.如果我在这里构建一个赝品,我唯一能想到的就是让赝品对硬编码输入做出某些错误响应,但同样,这感觉就像现在重新构建模拟框架.

Here, you can see that the function can throw different errors which will have different meanings to the API caller. If I was building a fake here, the only thing I can think to do is have the fake respond with certain errors to hard-coded inputs, but again, this just feels like re-building the mocking framework now.

所以,基本上,在一天结束时,我不确定您如何使用基于经典状态的断言方法在没有模拟的情况下编写单元测试,并且我很感激有关如何为我的代码示例执行此操作的任何建议以上.谢谢.

So, basically, at the end of the day, I'm unsure how you write unit tests without mocks using the Classical state-based assertion approach, and I'd appreciate any advice on how to do so for my code example above. Thanks.

推荐答案

您应该使用给定依赖项的真实实现还是伪造类?

Should you use the real implementation of a given dependency or a fake class?

正如您自己的经验所示,过度使用模拟会使测试变得脆弱.因此,如果有理由,您应该只使用模拟(或其他类型的测试替身).使用测试替身的充分理由是:

As your own experience shows, overutilization of mocks makes tests brittle. Therefore, you should only use mocks (or other kinds of test doubles) if there is a reason to do so. Good reasons for using test doubles are:

  • 您无法轻松地使依赖组件 (DOC) 的行为符合您的测试预期.例如,您的代码是健壮的,并检查另一个组件的返回状态是否指示某些故障.为了测试您的健壮性代码,您需要其他组件返回失败状态 - 但对于真实的组件,这可能非常难以实现,甚至不可能实现.
  • 调用 DOC 是否会导致任何非极端行为(日期/时间、随机性、网络连接)?例如,如果您的代码计算使用当前时间,那么使用真正的 DOC(即时间模块),每次测试运行都会得到不同的结果.
  • 您要测试的结果是否会是被测代码传递给 DOC 的一些数据,但 DOC 没有获取该数据的 API?例如,如果您的被测代码将其结果写入控制台(在本例中控制台是 DOC),但您的测试不可能查询控制台写入的内容.
  • 真实 DOC 的测试设置过于复杂和/或需要大量维护(例如,需要外部文件).例如,DOC 解析某个固定路径的配置文件.而且,对于不同的测试用例,您需要以不同的方式配置 DOC,因此您必须在该位置提供不同的配置文件.
  • 原始 DOC 为您的测试代码带来了可移植性问题.例如,如果您的函数 hashPassword 使用一些加密硬件来计算哈希,但该硬件(或正确的硬件版本)并非在执行单元测试的所有主机上都可用.
  • 使用原始 DOC 会导致构建/执行时间过长吗?
  • 是否存在 DOC 稳定性(成熟度)问题导致测试不可靠,或者更糟糕的是,DOC 是否尚未可用?
  • 也许 DOC 本身没有上述任何问题,但有其自身的依赖项,由此产生的一组依赖项会导致上述一些问题?
  • You can not easily make the depended-on-component (DOC) behave as intended for your tests. For example, your code is robust and checks if another component's return state indicates some failure. To test your robustness code, you need the other component to return the failure status - but this may be horribly difficult to achieve or even impossible with the real component.
  • Does calling the DOC cause any non-derministic behaviour (date/time, randomness, network connections)? For example, if the computations of your code use the current time, then with the real DOC (that is, the time module) you would get different results for each test run.
  • Would the result that you want to test be some data that the code under test passes to the DOC, but the DOC has no API to obtain that data? For example, if your code under test writes its result to the console (the console being the DOC in this case), but there is no possibility for your tests to query the console what was written to it.
  • The test setup for the real DOC is overly complex and/or maintenance intensive (like, need for external files). For example, the DOC parses some configuration file at a fixed path. And, for different test cases you would need to configure the DOC differently and thus you would have to provide a different configuration file at that location.
  • The original DOC brings portability problems for your test code. For example if your function hashPassword uses some cryptographic hardware to compute the hash, but this hardware (or the proper hardware version) is not available on all hosts where the unit-tests are executed.
  • Does using the original DOC cause unnacceptably long build / execution times?
  • Has the DOC stability (maturity) issues that make the tests unreliable, or, worse, is the DOC not even available yet?
  • Maybe the DOC itself does not have any of the abovementioned problems, but comes with dependencies of its own, and the resulting set of dependencies leads to some of the problems mentioned above?

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

For example, you (typically) don't mock standard library math functions like sin or cos, because they don't have any of the abovementioned problems.

这篇关于对遵循 CQS 的非平凡函数及其依赖项执行非模拟、基于状态的单元测试的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆