0


使用 Spring Boot 进行单元测试

本文主要从Spring BootJUnitMockito分层架构等方面对单元测试进行了理论和实践上的探讨,单元测试是软件开发中最重要的部分。

1. 介绍

本文将从单元测试相关的技术主题开始。在本文的技术部分之后,将介绍使用Spring Boot、JUnit和Mockito进行单元测试的实践。本系列的下一篇将介绍集成测试

  1. 单元

单元测试中的单元(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**我们可以为方法提供不同的参数。

每种测试方法都包含三个主要阶段。

  1. 已知:准备测试用例所需的对象。
  2. 时间:执行运行测试场景所需的操作。
  3. 然后:检查或验证预期结果。

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 毫秒。时间安排完美!


本文转载自: https://blog.csdn.net/lilinhai548/article/details/142092139
版权归原作者 码踏云端 所有, 如有侵权,请联系我们删除。

“使用 Spring Boot 进行单元测试”的评论:

还没有评论