ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [알림 서비스] 테스트 코드 작성
    STOVE DEVCAMP 3기/알림 서비스 2023. 3. 10. 04:20

    테스트 코드 작성 규칙을 세워 알림 서비스 구현 시 단위 테스트 코드 작성을 수행함

     

    📌 어느 부분을 작성했는지

    • 알림 서비스 구현 시 클린 아키텍처를 도입하여 아래와 같이 구성함 - 참고) 클린 아키텍처 도입

    • 따라서 패키지 구조는 다음과 같음

    • 헥사고날 아키텍처를 기반으로 비즈니스 로직을 포함한 내부 영역과 외부 영역을 분리하였고, 이를 기반으로 단위 테스트 코드 작성 부분은 아래의 3가지로 설정
      • adapter/in 패키지의 Web Adatper
      • adapter/out 패키지의 Persistence Adapter
      • application 패키지의 Service

     

    📌 어떤 방식으로 작성했는지

    공통

    • Given-When-Then 패턴 사용
      • 테스트 코드를 작성하는 표현 방식 중 하나임
      • Given : 테스트 하기 위해 기본적으로 세팅하는 값
      • When : 테스트 하기 위한 조건 지정
      • Then : 테스트 하기 위한 행위가 우리가 예상하는대로 동작하는지 검증하는 행동 / 절차
    • 성공, 실패 테스트 케이스 작성

     

    adapter/in 패키지의 Web Adatper

    • @WebMvcTest 사용
      • Application Context를 완전히 시작 시키지 않고, Web Layer에 대한 단위 테스트 수행을 위해 사용함
    • HTTP Request의 Param, Request Body에 대한 유효성 검사 수행

    작성 예시 - 알림 수신 디바이스 등록 API 호출

    @WebMvcTest(DeviceController.class)
    public class DeviceControllerTest {
     
        @Autowired
        MockMvc mockMvc;
     
        @MockBean
        RegisterDeviceUseCase registerDeviceUseCase;
     
        @MockBean
        DeleteDeviceUseCase deleteDeviceUseCase;
     
        @MockBean
        UpdateNotificationSettingUseCase updateNotificationSettingUseCase;
     
        @MockBean
        GetNotificationSettingUseCase getNotificationSettingUseCase;
     
        @Test
        @DisplayName("디바이스 등록 성공")
        void registerDeviceSuccess() throws Exception {
            //given
            String requestBody = """
                    {
                        "token": "clutter-token"
                    }
                    """;
     
            //when, then
            mockMvc.perform(post("/api/notification/device")
                            .header("userId", "clutter-user-id")
                            .content(requestBody)
                            .contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.code").value("001"))
                    .andExpect(jsonPath("$.message").value("성공했습니다."))
                    .andExpect(jsonPath("$.data").isEmpty());
        }
     
        @Test
        @DisplayName("디바이스 토큰 값이 null인 경우 예외 반환")
        void throwExceptionIfTokenNull() throws Exception {
            //given
            String requestBody = """
                    {
                        "token": null
                    }
                    """;
     
            //when, then
            mockMvc.perform(post("/api/notification/device")
                    .header("userId", "clutter-user-id")
                    .content(requestBody)
                    .contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().is4xxClientError())
                    .andExpect(jsonPath("$.code").value("-401"))
                    .andExpect(jsonPath("$.message").value("디바이스 토큰 값은 필수입니다."));
        }
    }
    • @MockBean으로 의존성 주입을 받아 테스트 진행함

     

    adapter/out 패키지의 Persistence Adapter

    • @DataMongoTest
      • MongoDB의 컴포넌트들을 중점적으로 테스트 가능
      • @Document를 스캔하여 MongoTemplate 생성
    • 실제 데이터베이스와 통신하여 로직이 예상과 동일하게 동작하는지 검증 수행

    작성 예시 - 디바이스 조회 로직

    @Disabled
    @ActiveProfiles("test")
    @DataMongoTest(excludeAutoConfiguration = MongoDBConfig.class)
    @Import({DevicePersistenceAdapter.class,
            DeviceMapper.class,
            TokenMapper.class,
            DeviceMongoTemplate.class,
            MongoDBTestConfig.class})
    class DevicePersistenceAdapterTest {
     
        @Autowired
        DevicePersistenceAdapter devicePersistenceAdapter;
     
        @Autowired
        DeviceRepository deviceRepository;
     
        @Autowired
        DeviceMongoTemplate deviceMongoTemplate;
     
        @AfterEach
        void cleanUp() {
            deviceRepository.deleteAll();
        }
     
        @Test
        @DisplayName("디바이스가 존재하는 경우 객체를 반환함")
        void findByUserIdAndTokenExist() {
            //given
            TokenDocument tokenDocument = TokenDocument.builder()
                    .value("clutter-token")
                    .timestamp(LocalDateTime.now()).build();
            DeviceDocument deviceDocument = DeviceDocument.builder()
                    .userId("clutter-user")
                    .pushNotificationOpt(true)
                    .tokenDocument(tokenDocument).build();
            deviceRepository.save(deviceDocument);
     
            //when
            Device device = devicePersistenceAdapter.findByUserIdAndToken("clutter-user", "clutter-token");
     
            //then
            assertThat(device.getToken().getValue()).isEqualTo("clutter-token");
            assertThat(device.getUserId()).isEqualTo("clutter-user");
        }
     
        @Test
        @DisplayName("디바이스가 존재하지 않는 경우 null을 반환함")
        void findByUserIdAndTokenNotExist() {
            //when
            Device device = devicePersistenceAdapter.findByUserIdAndToken("clutter-user", "clutter-token");
     
            //then
            assertThat(device).isNull();
        }
     
        ...
    }

     

    application 패키지의 Service

    • JUnit
    • 내가 작성한 로직이 예상과 동일하게 동작하는지 검증 수행

    작성 예시

    • 알림 목록 조회 시 사용되는 커서 페이지네이션 로직에 대한 검증 수행함
    • 분기문에 대한 테스트 케이스를 상세히 작성함
    public class GetNotificationServiceTest {
     
        private final GetNotificationPort getNotificationPort = Mockito.mock(GetNotificationPort.class);
        private final GetNotificationService getNotificationService = new GetNotificationService(getNotificationPort);
     
        @Test
        @DisplayName("알림 데이터 리스트의 크기가 (요청 pageSize + 1)보다 작으면 hasNextPage=false")
        void hasNextPageFalseIfListSizeLtPageSize() {
            //given
            NotificationListQueryReq listQueryReq = NotificationListQueryReq.builder()
                    .userId("clutter-user-id")
                    .direction(true)
                    .nextIndex(null)
                    .pageSize(20).build();
     
            List<Notification> notifications = createNotificationList(20);
            given(getNotificationPort.findAllByUserId(listQueryReq)).willReturn(notifications);
     
            //when
            NotificationListQueryRes listQueryRes = getNotificationService.getNotificationList(listQueryReq);
     
            //then
            assertThat(listQueryRes.getNotifications().size()).isEqualTo(20);
            assertThat(listQueryRes.getNextIndex()).isEqualTo("-1");
            assertThat(listQueryRes.isHasNextPage()).isFalse();
        }
     
        @Test
        @DisplayName("알림 데이터 리스트의 크기가 (요청 pageSize + 1) 이상이고 아래 방향 스크롤이면, nextIndex=리스트의 마지막 원소의 id이고 hasNextPage=true")
        void hasNextPageTrueIfListSizeGtePageSizeWithDownScroll() {
            //given
            NotificationListQueryReq listQueryReq = NotificationListQueryReq.builder()
                    .userId("clutter-user-id")
                    .direction(true)
                    .nextIndex(null)
                    .pageSize(20).build();
     
            List<Notification> notifications = createNotificationList(21);
            given(getNotificationPort.findAllByUserId(listQueryReq)).willReturn(notifications);
     
            //when
            NotificationListQueryRes listQueryRes = getNotificationService.getNotificationList(listQueryReq);
     
            //then
            assertThat(listQueryRes.getNotifications().size()).isEqualTo(20);
            assertThat(listQueryRes.getNextIndex()).isEqualTo("notification-id1");
            assertThat(listQueryRes.isHasNextPage()).isTrue();
        }
     
        @Test
        @DisplayName("알림 데이터 리스트의 크기가 (요청 pageSize + 1) 이상이고 위 방향 스크롤이면, nextIndex=리스트의 첫번째 원소의 id이고 hasNextPage=true")
        void hasNextPageTrueIfListSizeGtePageSizeWithUpScroll() {
            //given
            NotificationListQueryReq listQueryReq = NotificationListQueryReq.builder()
                    .userId("clutter-user-id")
                    .direction(false)
                    .nextIndex("notification-id1")
                    .pageSize(20).build();
     
            List<Notification> notifications = createNotificationList(21);
            given(getNotificationPort.findAllByUserId(listQueryReq)).willReturn(notifications);
     
            //when
            NotificationListQueryRes listQueryRes = getNotificationService.getNotificationList(listQueryReq);
     
            //then
            assertThat(listQueryRes.getNotifications().size()).isEqualTo(20);
            assertThat(listQueryRes.getNextIndex()).isEqualTo("notification-id21");
            assertThat(listQueryRes.isHasNextPage()).isTrue();
        }
     
        ...
    }

    댓글

Designed by Tistory.