本文主要从Spring Boot、JUnit、Mockito、分层架构等方面对单元测试进行了理论和实践上的探讨,单元测试是软件开发中最重要的部分。
1. 介绍
本文将从单元测试相关的技术主题开始。在本文的技术部分之后,将介绍使用Spring Boot、JUnit和Mockito进行单元测试的实践。本系列的下一篇将介绍集成测试。
- 单元
单元测试中的单元(Unit)一词指的是可以单独测试和处理的最小功能部分。我们在编写单元测试时会更清楚地理解这一点。
3. 用例
它描述了系统使用特定功能或特性的方式。用例用于理解、设计或测试系统的需求。它通常包括用户如何交互、对系统的期望以及应实现的结果等详细信息。
4. 边缘用例
它是软件必须处理意外或边缘情况的特定场景。边缘场景代表与典型计划不同或被认为是罕见的情况。这些状态可用于进行意外的用户登录、测试限制或发现系统中的错误。边缘情况通常在测试过程中被考虑在内,并用于测试系统的稳健性和稳定性。
5. 单元测试
单元一词在上面已经解释过了,单元测试涵盖了我们可以考虑然后编写的所有可能性。每个单元必须至少有一个测试方法。测试不是为方法编写的,而是为单元编写的。单元测试可以按以下顺序编写:快乐路径/用例、边缘情况和异常情况。这些步骤是必需的,但为什么呢?
它确保它能根据接受的输入产生正确的输出并表现出预期的行为。单元测试最适合于及早发现这些风险并修复错误,例如可能发生的意外情况、生产代码可能会更改、生产代码可能尚未准备好应对任何情况等。简而言之,单元测试可确保生产代码的安全。
单元测试的另一个重要方面是必须测试业务逻辑,而基础架构代码不在单元测试中测试。这些可以在集成测试中进行测试。您可以检查模式以分离业务和基础架构代码,即洋葱架构、六边形架构等。
单元测试的另一个优点是速度快,因为运行测试时不需要 Spring ApplicationContext。由于上下文的原因,同一金字塔中的集成测试比单元测试运行得慢得多。
6. 让我们编码吧
我编写了控制器层的测试,但我不会分享这些和其他细节。如果您对这些感兴趣,可以访问GitHub 存储库。
在分层架构的项目中,业务代码大多位于服务层。这意味着服务层有单元,必须进行测试。让我们专注于最重要的部分。
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final UserConverter userConverter;
@Override
public UserDTO saveUser(UserDTO userDTO) throws ControllerException {
validateUserDTO(userDTO);
User user = userConverter.convertToUser(userDTO);
try {
return userConverter.convertToUserDTO(userRepository.save(user));
} catch (Exception exception) {
throw new ControllerException(E_GENERAL_SYSTEM);
}
}
private void validateUserDTO(UserDTO userDTO) throws ControllerException {
Validate.stateNot(Objects.isNull(userDTO.getEmail()), E_USER_EMAIL_MUST_NOT_BE_NULL);
Validate.stateNot(findByEmail(userDTO.getEmail()).isPresent(), E_USER_ALREADY_REGISTERED);
}
@Override
public Optional<User> findByEmail(String email) {
return userRepository.findByEmail(email);
}
}
如上所示,有两个公共方法和一个私有方法,私有方法可以被认为是它们被使用的公共方法的一部分。并且有许多可能的情况,如果你想想你需要写多少个测试,答案是足以覆盖所有情况。
7. 一般的注释和方法
@ExtendWith用于将 Mockito 库集成到 JUnit 测试中。**@Test**标记一个方法以提供测试功能。测试方法包含指定的测试用例,并由 JUnit 自动运行。
我们需要模拟正在测试的类的依赖项。正如我上面所写的,原因是 Spring ApplicationContext 不支持,我们无法将依赖项注入上下文。**@Mock创建模拟依赖项,而@InjectMocks**注入依赖项。
@BeforeEach和**@AfterEach**可用于我们想要在每个方法运行之前和之后执行的操作。
@ParameterizedTest用于使用不同的参数值运行重复的测试用例。使用**@ValueSource**我们可以为方法提供不同的参数。
每种测试方法都包含三个主要阶段。
- 已知:准备测试用例所需的对象。
- 时间:执行运行测试场景所需的操作。
- 然后:检查或验证预期结果。
doReturn/when确定使用指定参数导航方法时的行为。但是,依赖项是 @Mock,永远不会真正运行。
verify用于检查测试下的代码是否按预期运行,如果有一个公共 void 方法,我们可以使用它来测试它。
断言用于验证预期结果。
@ExtendWith(MockitoExtension.class)
class UserServiceImplTest {
@InjectMocks
private UserServiceImpl userService;
@Mock
private UserRepository userRepository;
@Mock
private UserConverter userConverter;
private UserDTO userDTO;
public static final String MOCK_EMAIL = "[email protected]";
@BeforeEach
void setUp() {
System.out.println("setUp");
userDTO = new UserDTO();
}
@AfterEach
void tearDown() {
System.out.println("tearDown");
}
@ParameterizedTest
@ValueSource(strings = {"[email protected]", "[email protected]"})
@DisplayName("Happy Path Test: save user use cases")
void givenCorrectUserDTO_whenSaveUser_thenReturnUserDTO(String email) throws ControllerException {
// given
userDTO.setUserName("mertbahardogan").setEmail(email).setPassword("pass");
User savedUser = new User().setEmail(email);
doReturn(savedUser).when(userRepository).save(any());
doReturn(userDTO).when(userConverter).convertToUserDTO(any());
// when
UserDTO saveUser = userService.saveUser(userDTO);
// then
verify(userRepository, times(1)).findByEmail(anyString());
verify(userRepository, times(1)).save(any());
assertEquals(email, saveUser.getEmail());
}
@Test
@DisplayName("Exception Test: user email must not be null case")
void givenMissingUserDTO_whenSaveUser_thenThrowEmailMustNotNullEx() {
// when
ControllerException exception = assertThrows(ControllerException.class, () -> userService.saveUser(userDTO));
// then
assertNotNull(exception);
assertEquals(E_USER_EMAIL_MUST_NOT_BE_NULL, exception.getErrorMessage());
}
@Test
@DisplayName("Exception Test: user is already registered case")
void givenRegisteredUserDTO_whenSaveUser_thenThrowUserAlreadyRegisteredEx() {
// given
userDTO.setEmail(MOCK_EMAIL);
Optional<User> savedUser = Optional.of(new User().setEmail(MOCK_EMAIL));
doReturn(savedUser).when(userRepository).findByEmail(anyString());
// when
ControllerException exception = assertThrows(ControllerException.class, () -> userService.saveUser(userDTO));
// then
assertNotNull(exception);
assertEquals(E_USER_ALREADY_REGISTERED, exception.getErrorMessage());
}
@Test
@DisplayName("Happy Path Test: find user by email")
void givenCorrectUserDTO_whenFindByEmail_thenReturnUserEmail() {
// given
Optional<User> savedUser = Optional.of(new User().setEmail(MOCK_EMAIL));
doReturn(savedUser).when(userRepository).findByEmail(anyString());
// when
Optional<User> user = userService.findByEmail(MOCK_EMAIL);
// then
verify(userRepository, times(1)).findByEmail(anyString());
assertEquals(savedUser, user);
}
}
*UserServiceImpl *测试类运行了1 秒 761 毫秒。时间安排完美!
版权归原作者 码踏云端 所有, 如有侵权,请联系我们删除。