[사용 스택]
- Spring Boot 3.x
- Java 17
- Spring data JPA
- H2
- Junit 5
- mockito
레포지토리 테스트에 이어서 서비스에서 단위 테스트를 작성한 방법을 소개하겠습니다.
Service
서비스의 역할은 레포지토리를 사용하여 데이터를 요청하고
가져온 데이터로 비즈니스 로직을 수행하는 것입니다.
서비스를 테스트하기 위해서는 레포지토리를 주입받아야 하는데,
이 레포지토리가 실제 의존성을 주입받아 사용된다면,
서비스를 테스트하는 단위 테스트라고 말하기 어렵습니다.
(오히려 서비스와 레포지토리 모두를 한 번에 테스트하는 통합 테스트라고 생각합니다.)
그렇기 때문에 레포지토리의 의존성을 줄여줄 필요가 있습니다.
이를 위해 테스트 더블(Test Double)을 사용할 것입니다!!
테스트 더블
테스트 더블은 실제 객체를 대신해서 테스팅에 사용하는 방법들을 말합니다.
테스트 방법
우선 SpringBootTest 보다 가볍게 Mockito의 필요한 기능만을 사용할 수 있도록
아래 어노테이션을 클래스에 추가해 줍니다.
@ExtendWith(MockitoExtension.class)
그리고 테스트할 서비스는 @InjectMocks 어노테이션을 붙여 Mock 객체를 주입받습니다.
이 주입할 의존성들은 @Mock 어노테이션을 붙여줍니다.
레포지토리 테스트 때와 마찬가지로 Given/When/Then 패턴을 사용합니다!
Given에서는 레포지토리(테스트 더블)의 동작을 정의해 줍니다.
When에서는 서비스에 요청을 하고 응답 데이터를 받습니다.
Then에서는 verify 메서드를 통해 레포지토리가 동작했는지를 확인하거나 (Mock-행위검증)
assertThat 메서드를 통해 응답받은 데이터가 비즈니스로직을 통해 원하는 결괏값으로 나온 것인지를 확인합니다.(Stub-상태검증)
또한, 비즈니스 로직을 수행하는 중 발생 가능한 예외를 처리해주어야 합니다.
(저는 발생 가능한 여러 예외를 커스텀으로 만들어두었습니다.)
비즈니스 로직에서 예외가 발생되도록 Given에서 레포지토리의 반환 데이터를 정의해 주고
When에서 Assertions.assertThrow() 메서드를 통해 발생된 예외를 저장하여
Then에서 Assertions.assertThat() 메서드를 통해 발생된 예외가 내가 예상한 예외가 맞는지 확인해 줍니다.
아래는 제가 위 방법으로 작성한 테스트 코드입니다.
비즈니스 로직이 정확히 수행되었는지 확인을 위해 Stub을 사용해 필요한 데이터가 들어간 결괏값을 반환해 주었습니다.
// ...
@ExtendWith(MockitoExtension.class)
class DeviceServiceImplTest {
@InjectMocks
DeviceServiceImpl deviceService;
@Mock
DeviceRepository deviceRepository;
@Mock
LaptopInfoRepository laptopInfoRepository;
@Mock
MobileInfoRepository mobileInfoRepository;
@Test
public void findDeviceList() throws Exception {
//given
Device device = Device.builder()
.id(1L)
.deviceName("deviceName")
.releaseDate(LocalDateTime.now())
.launchPrice(1000)
.detailId("detailId")
.category(Category.createCategory("manufacturer", "deviceType"))
.build();
List<Device> content = new ArrayList<>(List.of(device));
Pageable pageable = PageRequest.of(0, 10);
Slice<Device> deviceSlice = new SliceImpl<>(content, pageable, false);
when(deviceRepository.findSliceBy(Mockito.any(Pageable.class)))
.thenReturn(deviceSlice);
Optional<MobileInfo> mobileInfo = Optional.of(MobileInfo.builder().image("image").build());
when(mobileInfoRepository.findById(Mockito.anyString()))
.thenReturn(mobileInfo);
FindDeviceListReqDTO request = FindDeviceListReqDTO.builder()
.page(0).size(10).build();
//when
FindDeviceListResDTO response = deviceService.findDeviceList(request);
//then
assertThat(response.getDeviceList().get(0).getImage()).isEqualTo(mobileInfo.get().getImage());
assertThat(response.getDeviceList().get(0).getDeviceName()).isEqualTo(content.get(0).getDeviceName());
assertThat(response.getHasNext()).isFalse();
}
@Test
public void findDeviceListThrowPageNotFoundException() throws Exception {
//given
List<Device> content = new ArrayList<>();
Pageable pageable = PageRequest.of(0, 10);
Slice<Device> deviceSlice = new SliceImpl<>(content, pageable, false);
when(deviceRepository.findSliceBy(Mockito.any(Pageable.class)))
.thenReturn(deviceSlice);
FindDeviceListReqDTO request = FindDeviceListReqDTO.builder()
.page(0).size(10).build();
//when
RestApiException exception = assertThrows(RestApiException.class, () -> {
deviceService.findDeviceList(request);
});
//then
assertThat(exception.getExceptionCode()).isEqualTo(CustomExceptionCode.PAGE_NOT_FOUND);
}
@Test
public void findDeviceListByCategory() throws Exception {
//given
Device device = Device.builder()
.id(1L)
.deviceName("deviceName")
.releaseDate(LocalDateTime.now())
.launchPrice(1000)
.detailId("detailId")
.category(Category.createCategory("manufacturer", "deviceType"))
.build();
List<Device> content = new ArrayList<>(List.of(device));
Pageable pageable = PageRequest.of(0, 10);
Slice<Device> deviceSlice = new SliceImpl<>(content, pageable, false);
when(deviceRepository.findSliceByCategory_Id(Mockito.any(Pageable.class), Mockito.anyLong()))
.thenReturn(deviceSlice);
Optional<MobileInfo> mobileInfo = Optional.of(MobileInfo.builder().image("image").build());
when(mobileInfoRepository.findById(Mockito.anyString()))
.thenReturn(mobileInfo);
FindDeviceListByCategoryReqDTO request =
new FindDeviceListByCategoryReqDTO(0, 10, 1L);
//when
FindDeviceListResDTO response = deviceService.findDeviceListByCategory(request);
//then
assertThat(response.getDeviceList().get(0).getImage()).isEqualTo(mobileInfo.get().getImage());
assertThat(response.getDeviceList().get(0).getDeviceName()).isEqualTo(content.get(0).getDeviceName());
assertThat(response.getHasNext()).isFalse();
}
@Test
public void findDeviceListByCategoryThrowPageNotFoundException() throws Exception {
//given
List<Device> content = new ArrayList<>();
Pageable pageable = PageRequest.of(0, 10);
Slice<Device> deviceSlice = new SliceImpl<>(content, pageable, false);
when(deviceRepository.findSliceByCategory_Id(Mockito.any(Pageable.class), Mockito.anyLong()))
.thenReturn(deviceSlice);
FindDeviceListByCategoryReqDTO request =
new FindDeviceListByCategoryReqDTO(0, 10, 1L);
//when
RestApiException exception = assertThrows(RestApiException.class, () -> {
deviceService.findDeviceListByCategory(request);
});
//then
assertThat(exception.getExceptionCode()).isEqualTo(CustomExceptionCode.PAGE_NOT_FOUND);
}
@Test
public void findMobileInfo() throws Exception {
//given
String detailId = "detailId";
Optional<MobileInfo> mobileInfo = Optional.of(MobileInfo.builder().id(detailId).modelname("modelName").build());
when(mobileInfoRepository.findById(Mockito.anyString()))
.thenReturn(mobileInfo);
FindDeviceInfoReqDTO request = new FindDeviceInfoReqDTO(detailId);
//when
FindDeviceInfoResDTO<MobileInfo> response = deviceService.findMobileInfo(request);
//then
assertThat(response.getInfo().getId()).isEqualTo(request.getDetailId());
assertThat(response.getInfo().getModelname()).isEqualTo(mobileInfo.get().getModelname());
}
@Test
public void findMobileInfoThrowDeviceNotFoundException() throws Exception {
//given
Optional<MobileInfo> mobileInfo = Optional.empty();
when(mobileInfoRepository.findById(Mockito.anyString()))
.thenReturn(mobileInfo);
String detailId = "detailId";
FindDeviceInfoReqDTO request = new FindDeviceInfoReqDTO(detailId);
//when
RestApiException exception = assertThrows(RestApiException.class, () -> {
deviceService.findMobileInfo(request);
});
//then
assertThat(exception.getExceptionCode()).isEqualTo(CustomExceptionCode.DEVICE_NOT_FOUND);
}
@Test
public void findLaptopInfo() throws Exception {
//given
String detailId = "detailId";
Optional<LaptopInfo> laptopInfo = Optional.of(LaptopInfo.builder().id(detailId).modelname("modelName").build());
when(laptopInfoRepository.findById(Mockito.anyString()))
.thenReturn(laptopInfo);
FindDeviceInfoReqDTO request = new FindDeviceInfoReqDTO(detailId);
//when
FindDeviceInfoResDTO<LaptopInfo> response = deviceService.findLaptopInfo(request);
//then
assertThat(response.getInfo().getId()).isEqualTo(request.getDetailId());
assertThat(response.getInfo().getModelname()).isEqualTo(laptopInfo.get().getModelname());
}
@Test
public void findLaptopInfoThrowDeviceNotFoundException() throws Exception {
//given
Optional<LaptopInfo> laptopInfo = Optional.empty();
when(laptopInfoRepository.findById(Mockito.anyString()))
.thenReturn(laptopInfo);
String detailId = "detailId";
FindDeviceInfoReqDTO request = new FindDeviceInfoReqDTO(detailId);
//when
RestApiException exception = assertThrows(RestApiException.class, () -> {
deviceService.findLaptopInfo(request);
});
//then
assertThat(exception.getExceptionCode()).isEqualTo(CustomExceptionCode.DEVICE_NOT_FOUND);
}
}
내 코드에서 아쉬운 점
레포지토리 테스트 글에도 비슷한 말을 했었지만,
테스트에 사용될 엔티티 객체들을 만들어줄 팩토리 메서드들을 모아둔 클래스를 만들어서
필요한 객체를 만들도록 개선하는 것이 필요해 보입니다.
또한, Mockito.mock() 메서드로 사용하지 않는 데이터를 만들어 채웠는데
이는 메서드의 적절한 역할과는 다르게 쓰였으므로 위 방법으로 더미 데이터를 만들어주는 것이
더 올바른 방법이라고 생각합니다.
이 방법을 테스트 픽스처로 준비한다면 보다 좋은 테스트 코드일 것이라고 확신합니다!!
결론
다른 서비스에서는 중복된 로직을 private 메서드로 추출해 둔 것도 있는데,
제가 이전에 작성한 글을 참고해 주시면 도움이 될 것 같습니다!
다음에는 컨트롤러를 테스트하겠습니다!
'WEB > Spring' 카테고리의 다른 글
SpringDataJpa와 QueryDsl 함께 사용하기 (feat. @DataJpaTest) (1) | 2024.12.16 |
---|---|
단위 테스트 - Controller (feat. 추가적인 단위 테스트의 방향성) (0) | 2024.08.31 |
단위 테스트 - Repository (feat. 테스트 픽스쳐) (0) | 2024.08.27 |
Service의 메서드들이 중복해서 사용하는 로직을 테스트하기 (0) | 2024.08.20 |
RestControllerAdvice로 전역에서 발생된 예외 처리하기. (0) | 2024.05.01 |