【问题标题】:TDD dilemma: Testing behavior instead of testing state VS Tests should be unaware of implementationTDD 困境:测试行为而不是测试状态 VS 测试应该不知道实现
【发布时间】:2015-11-21 16:51:49
【问题描述】:

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

有一些 TDD 规则:

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

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

@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() 来实现我的服务。

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

【问题讨论】:

  • usersService 是否调用 findByEmail()?还是存在()?我不明白你的问题。
  • 我不知道。我的服务尚未实施。但问题是如何编写好的测试。测试不应影响我的生产代码。

标签: spring junit mocking tdd mockito


【解决方案1】:

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

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

您对于是通过电子邮件检索用户还是使用布尔值exists() 调用来检查其存在的困境对我来说听起来像是YAGNI 的情况。如果除了检查它是否为空之外,您不知道要对检索到的 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
}

顺便说一句,(这是一个附带问题,有很多不同的方法可以在 StackOverflow 的其他地方介绍测试中的异常)如果能够将 expect() 调用放在"then" 部分,但必须在 signUp() 之前。您也可以选择(如果您不想在“给定”部分调用 expect())使用 @Testexpected 参数而不是调用 expect()。显然,JUnit 5 将允许将抛出调用包装在期望调用中,该调用返回抛出的异常,如果没有抛出异常则失败。

【讨论】:

    【解决方案2】:

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

    将测试集中在一个行为上:

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2010-10-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-07-11
      • 1970-01-01
      • 2021-09-11
      • 2013-08-11
      相关资源
      最近更新 更多