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

时间:2015-11-21 16:51:50

标签: spring junit mocking tdd mockito

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

有一些TDD规则:

  1. 测试行为而不是状态。
  2. 测试不应该依赖于 实施
  3. 我创建了UsersService空类,它取决于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()来实现我的服务。

    这个测试应该怎么样?

2 个答案:

答案 0 :(得分:2)

您的测试似乎反映了对行为和实施的一些混淆。当你第一次打电话给signUp()时,你似乎期望状态发生变化,但是因为你使用的是模拟我认为不会发生这种情况,所以如果你这么做就不要再打signUp()两次重新使用模拟(并且expect()应该在signUp()之前,我相信)。如果你没有使用模拟,那么两次调用signUp()将是一个有效的测试,没有实现依赖性,但你(明智地,恕我直言)使用模拟来避免缓慢的,依赖于数据库的测试,以便轻松模拟依赖关系,所以只需调用signUp()一次,让模拟模拟状态。在测试服务行为时模拟存储接口是有意义的。

至于你的2个测试规则,你不能在没有实现概念的情况下使用模拟(我更喜欢将其视为“交互” - 特别是如果你模拟接口而不是具体的类)。您似乎采用模块化设计,因此我不担心模拟明显的交互。如果你稍后改变主意关于交互(无论你是否应该检索用户对象而不是布尔存在检查),你改变你的测试 - 没什么大不了的,恕我直言。进行单元测试应该会让你害怕改变你的代码。如果需要改变相互作用,模拟可以使测试更加脆弱。另一方面是你在编码之前更多地考虑这些交互,这很好,但不要卡住。

关于是否通过电子邮件检索用户或使用布尔exists()调用检查其存在的困境听起来像是YAGNI的情况。如果你不知道你要检索的User对象是什么,除了检查它是否为null之外,请使用布尔值。如果你以后改变主意,可能会有一些破坏的测试(轻松)修复,但你会更清楚地了解事情应该如何运作。

因此,如果您决定坚持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
}

BTW,(这是一个侧面问题,并且有许多不同的方法可以在StackOverflow上的其他地方进行测试中期待异常)能够进行expect()调用会很好在“then”部分,但它必须在signUp()之前。您也可以(如果您不想在“给定”部分中调用expect()),请使用expected的{​​{1}}参数,而不是调用@Test。显然,JUnit 5允许在一个期望调用中包装抛出调用,该调用返回抛出的异常,如果没有抛出则失败。

答案 1 :(得分:0)

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

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

@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);
}