[Spring] JUnit5로 Mocking 및 슬라이스 테스트하기

2021년 09월 09일 (1달 전)
JUnit5로 스프링 프로젝트 효과적으로 테스트하기에서 JUnit를 통해 전반적으로 테스트를 해보는 방법에 대해서 작성했는데, 사실 이 테스트 방법은 그리 좋은 방법은 아니다. 왜냐하면 Unit Test 원칙을 제대로 준수하지 않았고, 사실상 단위 테스트가 아니라 통합 테스트이기 때문이다. 이번에는 Slice Test와 Mocking 기법을 적용하여 지난번에 작성한 테스트를 좋은 단위 테스트로 바꿔보려고 한다. 예제 코드

Spring Boot 슬라이스 테스트

슬라이스 테스트란?

테스트를 위해 생성된 ApplicationContext를 분할해서 테스트를 하는 것을 말한다.

슬라이스 테스트를 하는 이유

@SpringBootTest 어노테이션을 사용하게 되면 실제 구동되는 애플리케이션의 설정, 모든 Bean을 로드하기 때문에 시간이 오래 걸리고 무거다. 그리고 테스트 단위가 크기 때문에 정작 테스트하려는 코드가 아닌 다른 곳에서 오류가 날 수 있어 디버깅이 어려운 편이다. 결과적으로 웹을 실행시키지 않고 테스트 코드를 통해 빠른 피드백을 받을 수 있다는 장점이 희석된다.

따라서 단위 테스트에 있어서 @SpringBootTest를 사용하를 하게 될 경우 자연스럽게 통합 테스트가 되게 된다.

슬라이스 테스트를 위한 어노테이션들

슬라이스 테스트를 위한 많은 어노테이션을 지원하니 알아뒀다가 사용하면 될 것 같다.

  • @DataCassandraTest
  • @DataJdbcTest
  • @DataJpaTest
  • @DataLdapTest
  • @DataMongoTest
  • @DataNeo4jTest
  • @DataR2dbcTest
  • @DataRedisTest
  • @JdbcTest
  • @JooqTest
  • @JsonTest
  • @RestClientTest
  • @WebFluxTest
  • @WebMvcTest
  • @WebServiceClientTest

Mocking 관련 어노테이션 및 메서드

@Mock VS @MockBean

이번 예제에서는 스프링 컨테이너에 등록해야 하기 때문에 @MockBean을 사용해 테스트를 진행한다.

@Mock - 목 객체가 스프링 컨테이너에 등록되지 않아도 될 경우 사용

@MockBean - 목 객체를 스프링 컨테이너에 등록해야 할 경우 사용

org.mockito.Mockito.when()

특정 메서드가 실행될 경우 특정 값을 반환하게 하고 싶을 경우 사용한다. Mocking 객체는 껍데기만 있는 것이기 때문에 실질적인 구현을 여기다가 한다고 보면 된다.

Repository 테스트

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByName(String name);
}
@ExtendWith(SpringExtension.class)
@DataJpaTest
public class UserRepositoryTests {
    @Autowired
    private UserRepository userRepository;

    @Test
    @DisplayName("이름으로 유저 정보 조회 테스트")
    public void findByNameTest() {
        String name = "gunkim";
        int age = 22;
        userRepository.save(User.builder()
                .name(name)
                .age(age)
                .build());

        User user = userRepository.findByName(name).get();

        assertThat(user.getName(), is(equalTo(name)));
        assertThat(user.getAge(), is(equalTo(age)));
    }
}

Repository에서 개인적으로 findAll(), findById() 등 기본적으로 제공하는 메서드를 제외한 커스텀 메서드를 테스트하면 된다. 이전 코드와 다른 점은 @SpringBootTest -> @DataJpaTest 로 변경해서 Jpa 관련 Bean들만 스프링 컨텍스트에 로드하여 Slice Test를 하게 되므로 테스트 속도 실행 속도가 빠르게 된다.

Service 테스트

@RequiredArgsConstructor
@Service
public class UserServiceImpl implements UserService {
    private final UserRepository userRepository;

    @Override
    public UserInfoResponseDto selectUserInfo(String name) throws IllegalArgumentException {
        User user = userRepository.findByName(name)
                .orElseThrow(() -> new IllegalArgumentException(name+": 유저를 찾지 못했습니다."));
        return new UserInfoResponseDto(user);
    }
}
@ExtendWith(SpringExtension.class)
public class UserServiceTests {
    private UserService userService;
    @MockBean
    private UserRepository userRepository;

    @BeforeEach
    public void setup() {
        this.userService = new UserServiceImpl(userRepository);
    }

    @Test
    @DisplayName("이름으로 유저 정보 조회 테스트, entity -> dto 변환")
    public void selectUserInfo() {
        String name = "gunkim";
        int age = 22;
        when(userRepository.findByName(any(String.class))).thenReturn(Optional.of(User.builder()
                .name(name)
                .age(age)
                .build()));

        UserInfoResponseDto dto = userService.selectUserInfo(name);
        assertThat(dto.getName(), is(equalTo(name)));
        assertThat(dto.getAge(), is(equalTo(age)));
    }
}

@SpringBootTest 어노테이션을 제거하고, Mock객체(가짜 객체)를 주입하여 Service 객체를 테스트한다. 이렇게 되면 불필요한 Spring Bean들을 로드하지 않게 되어 실행 속도도 빨라지고 Service가 의존하고 있는 Repository 객체를 가짜 객체로 Mocking하여 Service 로직만을 온전히 테스트할 수 있게 된다.

Controller 테스트

@RequiredArgsConstructor
@RestController
@RequestMapping("/api/user")
public class UserController {
    private final UserService userService;

    @GetMapping
    public ResponseEntity<UserInfoResponseDto> getUserInfo(@RequestParam("username") String name) throws IllegalArgumentException {
        return ResponseEntity.ok(userService.selectUserInfo(name));
    }
}
@ExtendWith(SpringExtension.class)
@WebMvcTest(UserController.class)
public class UserControllerTests {
    @Autowired
    private MockMvc mockMvc;
    @Autowired
    private ObjectMapper objectMapper;

    @MockBean
    private UserService userService;

    @Test
    @DisplayName("/api/user 테스트")
    public void getUserInfoApiTest() throws Exception {
        String name = "gunkim";
        int age = 22;
        when(userService.selectUserInfo(any(String.class))).thenReturn(UserInfoResponseDto.builder()
                .name(name)
                .age(age)
                .build());

        MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/api/user")
                .param("username", name))
                .andExpect(status().isOk())
                .andDo(print())
                .andReturn();

        UserInfoResponseDto dto = objectMapper.readValue(result.getResponse().getContentAsString(), UserInfoResponseDto.class);
        assertThat(dto.getName()).isEqualTo(name);
        assertThat(dto.getAge()).isEqualTo(age);
    }
}

Service 테스트와 다를 게 없다. 다만 @SpringBootTest -> @WebMvcTest(UserController.class)로 변경되었다.

마지막으로

통합 테스트가 필요한 경우가 아니라면 슬라이스 테스트를 하도록 하자