인프런 커뮤니티 질문&답변

개발하는쿼카님의 프로필 이미지

작성한 질문수

Practical Testing: 실용적인 테스트 가이드

시간대에 따라서 변화하는 로직 테스트 하는 가이드

해결된 질문

23.08.17 00:43 작성

·

502

·

수정됨

0

안녕하세요. 우빈님

 

시간대에 따라 다른 결과를 주는 로직을 테스트 하는 과정에서 고민이 있어 질문을 드립니다.

 

우빈님이 강의에서도 언급하셨지만, 시간과 같이 관측할 때마다 달라지는 영역은 외부로 분리하면 테스트하기 쉬워진다고 말씀하셨습니다.!

하지만, 현재 서비스 레이어 까지만 분리가 가능하고 컨트롤러에서 LocalDateTime 파라미터로 받지 못하는 상황입니다.

 

그래서 컨트롤러를 테스트할 때 어떻게 해야할까 고민을 좀 해봤는데요.

 

저의 결론은 TimeProvider라는 클래스를 하나 만들어서

컨트롤러를 테스트할 때는 이를 mocking하는 방식으로 테스트 코드를 작성했습니다.

 

TimeProvider

/**
 * 시간을 고정하여 테스트하기 위해 사용
 */
@Component
public class TimeProvider {
    public LocalDateTime getCurrentLocalDateTime() {
        return LocalDateTime.now();
    }
}

 

Controller

  • GET을 사용하고 싶은데, 외부와 연동해야해서 POST를 사용할 수 밖에 없습니다. ㅠㅠ

/**
 * 인기 메뉴 조회
 * 현재 시간대의 식사종류와 일치하는 가장 조회수가 많은 금일 식사 메뉴 조회
 */
@PostMapping("/menu/top1-view")
public ResponseEntity<SkillResponse> getTop1RestaurantMenuByView(@RequestBody SkillPayload payload, @PageableDefault(size = 1) Pageable pageable) {
    log.info("request={}", payload);

    Page<RestaurantMenuResponse> top1RestaurantMenuByView = restaurantService.findTop1RestaurantMenuByView(pageable, timeProvider.getCurrentLocalDateTime());
    RestaurantsMenuResponse response = new RestaurantsMenuResponse(top1RestaurantMenuByView.getContent());

    return new ResponseEntity<>(response.toSkillResponseUseTextCard(apiVersion), HttpStatus.OK);
}

 

Test Code

@Test
@DisplayName("추천수 가장 많은 메뉴를 1개 조회한다.")
void getTop1UosRestaurantMenuByView() throws Exception {
    // given
    // 현재 시간을 고정할 시간 생성
    LocalDateTime fixedDateTime = LocalDateTime.of(2023, 8, 16, 10, 59, 59);
    when(timeProvider.getCurrentLocalDateTime()).thenReturn(fixedDateTime);

    String date = CrawlingUtils.toDateString(fixedDateTime);
    restaurant restaurant1 = createUosRestaurant(date, STUDENT_HALL, MealType.BREAKFAST, "라면", 0, 0);
    restaurant restaurant2 = createUosRestaurant(date, MAIN_BUILDING, MealType.BREAKFAST, "김밥", 1, 0);
    restaurant restaurant3 = createUosRestaurant(date, WESTERN_RESTAURANT, MealType.BREAKFAST, "돈까스", 2, 0);
    restaurant restaurant4 = createUosRestaurant(date, MUSEUM_OF_NATURAL_SCIENCE, MealType.BREAKFAST, "제육", 2, 1);
    restaurantRepository.saveAll(List.of(restaurant1, restaurant2, restaurant3, restaurant4));

    SkillPayload skillPayload = createSkillPayload(RestaurantName.STUDENT_HALL.name(), MealType.BREAKFAST.name());

    // when // then
    mockMvc.perform(post("/api/v1/text-card/restaurant/menu/top1-view")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(om.writeValueAsBytes(skillPayload))
                    .content(om.writeValueAsString(PageRequest.of(0, 1))))
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.version").value(SkillResponse.apiVersion))
            .andExpect(jsonPath("$.template").isNotEmpty())
            .andExpect(jsonPath("$.template.outputs").isArray())
            .andExpect(jsonPath("$.template.outputs[0].textCard").isNotEmpty())
            .andExpect(jsonPath("$.template.outputs[0].textCard.text").isString());
}

 

이런식으로 작성하는 것이 최선일까요?

LocalDateTime fixedDateTime = LocalDateTime.of(2023, 8, 16, 10, 59, 59);
when(timeProvider.getCurrentLocalDateTime()).thenReturn(fixedDateTime);

TimeProvider 클래스를 만들어서 mocking 하는 방법이 최선일까요?



혹시 더 좋은 방법을 말씀주시면 감사하겠습니다.!

 

좋은 강의 만들어 주셔서 감사합니다.^^

답변 2

1

박우빈님의 프로필 이미지
박우빈
지식공유자

2023. 08. 20. 17:34

안녕하세요, 개발하는쿼카님! :)

먼저 주어진 상황에 따라 조금 다를 것 같은데요, 만약 제가 강의 중에 말씀드린 것처럼 Service 레이어를 mocking한 Controller 테스트를 작성한다면, any(LocalDateTime.class) 와 같이 Service 호출 로직을 mocking하여 테스트를 작성할 수 있을 것 같아요.

만약 Service 레이어를 mocking을 통해 분리하는 방식이 아니라면, 말씀해 주신대로 현재시간을 주입하는 빈을 사용하는 것도 좋은 방법일 수 있겠네요.

지금 Controller 로직을 봤을 때, 저라면 Service에서 한번에 Response를 응답하고, Controller는 비즈니스 로직이 존재하지 않기 때문에 Service를 mocking하고 외부에서 넘어오는 파라미터 정도만 검증할 것 같아요.

참고하셔서 결정하시면 될 것 같습니다. ㅎㅎ
감사합니다. :)

개발하는쿼카님의 프로필 이미지

2023. 08. 20. 19:29

상세한 답변 감사합니다.

0

인프런 AI 인턴님의 프로필 이미지

2023. 08. 19. 00:46

안녕하세요, 인프런 AI 인턴이에요.

시간대에 따라 다른 결과를 주는 로직을 테스트하기 위해 컨트롤러에서 LocalDateTime을 파라미터로 받지 못하는 상황이라고 하셨죠?

컨트롤러를 테스트할 때는 TimeProvider 클래스를 mocking하여 테스트 코드를 작성하셨다고 말씀하셨습니다.

이 방법은 좋은 접근 방법입니다. TimeProvider 클래스를 작성하여 현재 시간을 반환하는 메서드를 mocking하여 컨트롤러에서 사용되는 LocalDateTime 값을 테스트할 수 있게 됩니다.

시간과 같이 외부에 종속적인 요소들을 분리하여 테스트하기 위해 외부 인터페이스를 mocking하는 것은 일반적으로 좋은 방법입니다. 이렇게 하면 테스트 코드에서는 고정된 값을 사용하여 원하는 테스트 시나리오를 구현할 수 있게 됩니다.

현재 상황에서 서비스 레이어까지만 분리가 가능하다고 하셨는데, 컨트롤러에서 LocalDateTime을 파라미터로 받지 못한다면, TimeProvider 클래스를 이용하여 mocking한 값을 사용하여 테스트하는 방법이 적절한 방법인 것 같습니다.

그러나 GET 요청을 테스트하려면 POST 방식을 사용하는 것이 아니라 GET 방식을 사용해야 합니다. GET 요청은 URL에 매개변수를 함께 전달하여 호출하는 방식입니다. 따라서, 컨트롤러에서 사용하는 LocalDateTime을 mocking한 값을 URL에 매개변수로 전달하여 테스트할 수 있게 됩니다.

이렇게 GET 요청을 테스트하기 위해 URL에 매개변수를 전달하는 방식을 사용하시면 될 것 같습니다.

더 자세한 내용이나 다른 궁금한 점이 있다면 언제든지 물어보세요. 좋은 하루 되세요!