TDD 困境:测试行为而不是测试状态 VS 测试应该不知道实现
TDD dilemma: Testing behavior instead of testing state VS Tests should be unaware of implementation
我正在尝试使用 TDD 技术实现我的 Spring 网站。
有一些 TDD 规则:
- 测试行为而不是状态。
- 测试不应该依赖于
实施。
我创建了空的 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())使用 @Test
的 expected
参数而不是调用 expect()
。显然,JUnit 5 将允许将抛出调用包装在期望调用中,即 returns 抛出异常或在抛出 none 时失败。
我正在尝试使用 TDD 技术实现我的 Spring 网站。
有一些 TDD 规则:
- 测试行为而不是状态。
- 测试不应该依赖于 实施。
我创建了空的 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())使用 @Test
的 expected
参数而不是调用 expect()
。显然,JUnit 5 将允许将抛出调用包装在期望调用中,即 returns 抛出异常或在抛出 none 时失败。