TDD 困境:测试行为而不是测试状态 VS 测试应该不知道实现

TDD dilemma: Testing behavior instead of testing state VS Tests should be unaware of implementation

我正在尝试使用 TDD 技术实现我的 Spring 网站。

有一些 TDD 规则:

  1. 测试行为而不是状态。
  2. 测试不应该依赖于 实施。

我创建了空的 UsersService class,它依赖于 crud UsersRepository。 现在,我正在尝试编写注册新用户的测试,但我不知道如何正确地执行此操作。

@Test
public void signUp_shouldCheckIfUserExistsBeforeSign() throws ServiceException {
    // given
    User user = new User();
    user.setEmail(EMAIL);
    when(usersRepository.save(user)).thenReturn(user);
    when(usersRepository.exists(anyString())).thenReturn(Boolean.FALSE);

    // when
    usersService.signUp(user);

    // then
    thrown.expect(UserAlreadyExistsServiceException.class);
    usersService.signUp(user);
}

此代码测试行为,但也强制我使用 exists() 方法而不是 findByEmail() 来实现我的服务。

这个测试应该是什么样的?

测试行为很好,但生产代码需要表现出这种行为,这会在一定程度上影响实现。

将测试重点放在单一行为上:

@Test
public void signUpFailsIfUserEmailAlreadyExists() throws ServiceException {
    // given
    User user = new User();
    user.setEmail(EMAIL);
    when(usersRepository.emailExists(EMAIL)).thenReturn(Boolean.TRUE);

    // when
    usersService.signUp(user);

    // then
    thrown.expect(UserAlreadyExistsServiceException.class);
}

您的测试似乎反映了对行为和实现的一些混淆。第一次调用 signUp() 时,您似乎希望状态发生变化,但是因为您使用的是模拟,所以我认为这不会发生,所以如果您不调用 signUp() 两次重新使用模拟(我相信 expect() 应该在 signUp() 之前)。如果您不使用模拟,那么调用 signUp() 两次是一个有效的测试,没有实现依赖性,但是您(明智地,恕我直言)使用模拟来避免缓慢的、依赖于数据库的测试,以便于模拟依赖项,所以只调用 signUp() 一次,让模拟模拟状态。在测试您的服务行为时模拟您的存储接口是有意义的。

至于你的 2 条测试规则,你不能在没有实现概念的情况下使用模拟(我更愿意将其视为 "interactions" - 特别是如果你模拟接口而不是具体 类 ).你似乎有一个模块化设计,所以我不会担心模拟一个明显的交互。如果您稍后改变了对交互的想法(是否应该检索用户对象而不是布尔值存在检查),您就更改了测试 - 没什么大不了的,恕我直言。进行单元测试应该让您不那么害怕更改代码。如果需要更改交互,模拟确实会使测试更加脆弱。不利的一面是,您在编写代码之前会更多地考虑这些交互,这很好,但不要陷入困境。

关于是通过电子邮件检索用户还是使用布尔 exists() 调用检查其存在的两难选择对我来说听起来像是 YAGNI 的情况。如果除了检查它是否为 null 之外,您不知道要对检索到的 User 对象做什么,请使用布尔值。如果您稍后改变主意,您可能需要(轻松)修复一些损坏的测试,但您会对事情应该如何工作有更清晰的认识。

因此,如果您决定坚持使用 exists():

,您的测试可能看起来像这样
@Test
public void signUp_shouldCheckIfUserExistsBeforeSign() throws ServiceException {
    // given
    User user = new User();
    user.setEmail(EMAIL);
    when(usersRepository.exists(anyString())).thenReturn(Boolean.FALSE);
    thrown.expect(UserAlreadyExistsServiceException.class);

    // when
    usersService.signUp(user);

    // then - no validation because of expected exception
}

顺便说一句,(这是一个附带问题,有很多不同的方法可以在 Whosebug 的其他地方介绍测试中的异常)能够把 expect() 在 "then" 部分调用,但必须在 signUp() 之前。您也可以(如果您不想在 "given" 部分调用 expect())使用 @Testexpected 参数而不是调用 expect()。显然,JUnit 5 将允许将抛出调用包装在期望调用中,即 returns 抛出异常或在抛出 none 时失败。