테스트 코드 작성하면서 발생한 비극

엉금엉금·2022년 7월 13일
0

오늘 만난 문제

목록 보기
14/24

사건의 시작

시간 순서대로 발생한 일을 얘기해보겠다
때는 바야흐로 프로젝트의 API에 대한 테스트 코드를 작성하던 때였다
여섯개의 컨트롤러가 있지만 우선 두 개의 컨트롤러 메서드에 대한 테스트를 작성해보았다.
음식점을 초기에 등록 요청을 하는 API는 그럭저럭 테스트 작성을 잘했다. 이를통해 약간의 자신감을 얻었다.
여기에서 간단한 내용을 확인해 볼 수 있다ㅎㅎ

문제는 다음 테스트를 작성하는 중에 나오게 된다. 자신감이 자만이된 순간이라 생각한다ㅋㅋㅋ
우선 API의 요구사항은 '음식 주문'이다. 사용자가 선택한 특정 음식점의 번호와 하나 이상의 음식의 번호와 수량으로 음식 주문을 하는 것이다.

@AutoConfigureMockMvc
@SpringBootTest
class OrderControllerV1Test {

    @Autowired
    MockMvc mockMvc;

    @Autowired
    EntityManager em;

    @Autowired
    RestaurantRepository restaurantRepository;

    @Autowired
    FoodRepository foodRepository;

    @Autowired
    OrderRepository orderRepository;

    @BeforeEach
    void clear() {
        restaurantRepository.deleteAll();
        foodRepository.deleteAll();
        orderRepository.deleteAll();
    }

    @Test
    @Transactional
    @DisplayName("주문_음식_요청_테스트")
    void saveOrder() throws Exception {
        // given
        Restaurant savedRestaurant = restaurantRepository.save(new Restaurant("쉐이크쉑 청담점", 5000, 2000));
        foodRepository.save(new Food(savedRestaurant, "쉐이크쉑 버거", "10900"));
        foodRepository.save(new Food(savedRestaurant, "치즈 감자튀김", "4900"));
        foodRepository.save(new Food(savedRestaurant, "쉐이크", "5900"));

        List<OrderFoodInfoDto> foods = new ArrayList<>();
        foods.add(new OrderFoodInfoDto(1L, 1));
        foods.add(new OrderFoodInfoDto(2L, 2));
        foods.add(new OrderFoodInfoDto(3L, 3));

        OrderFoodRequestDto request = OrderFoodRequestDto.builder()
                .restaurantId(1L)
                .foods(foods)
                .build();

        ObjectMapper objectMapper = new ObjectMapper();
        String json = objectMapper.writeValueAsString(request);

        // when
        mockMvc.perform(post("/api/v1/orders")
                        .contentType(APPLICATION_JSON)
                        .content(json)
                )
                .andExpect(status().isOk())
                .andDo(print());

        em.flush();
        em.clear();

        // then
        //assertEquals(1L, orderRepository.count());
        Order order = orderRepository.findAll().get(0);
        int totalPrice = 0;

        List<OrderFood> orderFoodList = order.getOrderFoods();
        for (OrderFood orderFood : orderFoodList) {
            Food food = orderFood.getFood();
            totalPrice += ( Integer.parseInt(food.getPrice()) * orderFood.getQuantity() );
        }

        assertEquals(totalPrice + order.getRestaurant().getDeliveryFee(), 40400);
    }
}

내용은 같지만 이름이 다른 JSON Array 2개

  • 테스트 진행을 위해서는 '음식점 기본 정보'와 '음식점의 음식' 정보가 DB에 저장되어 있어야 한다

  • 이후 MockMvc를 이용하여 요청이 성공적으로 들어가는지 알아보기 위하여 요청 데이터를 만들어야 했다. 따라서 실제 컨트롤러의 메서드에서 받는 DTO를 기반으로 데이터를 만들고 'ObjectMapper'를 이용하여 JSON으로 만들어준다.

  • 테스트는 성공적으로 동작한다... 그러나 API요구 명세서 상의 요청 예시와 테스트 상의 결과가 달랐다.

  • 테스트의 결과는 다음과 같다

  • 설마 테스트 코드를 디버깅할까 싶었지만...(현업에서 하는지 모르겠지만...) 우선 디버깅을 해보았다 그런데 위에 코드에서 objectMapper.writeValueAsString(request) 이후 '음식번호'와 '음식수량'에 관한 리스트가 다른 이름으로 2개 들어가 있었다.

이미지가 잘 보이지 않는다;; 아래와 같다 'foods'와 'orderFoodInfoList'를 유심히 보자!
{"restaurantId":1,"foods":[{"id":1,"quantity":1},{"id":2,"quantity":2},{"id":3,"quantity":3}],"orderFoodInfoList":[{"id":1,"quantity":1},{"id":2,"quantity":2},{"id":3,"quantity":3}]}

  • 해당 문제는 DTO클래스의 @Getter로 제공하는 이외의 관계없는 getter를 내가 만들어두었기 때문이었다. 어이가 없었지만 ObjectMapper라는 녀석이 그런가보다 싶었다.
@Getter // 클래스명 변경 FoodOrderRequestDto -> OrderFoodRequestDto
@NoArgsConstructor
public class OrderFoodRequestDto {
    private Long restaurantId;
    private List<OrderFoodInfoDto> foods;

    public List<OrderFoodInfoDto> getOrderFoodInfoList() {
        return foods;
    }
}
  • 보다시피 'foods'라는 리스트를 얻을 수 있는 별도의 getter를 내가 만들어 두었다. 이유는 해당 DTO클래스에서 foods를 얻을 때 OrderFoodInfoList로 하면 가독성이 좋아질 수 있을 것이라 판단했기 때문이다. 하지만 현실은 나에개 바르지 않은 테스트 결과를 전달해 주었다. 결과적으로는 'getOrderFoodInfoList'라는 getter 메서드를 없애므로써 올바른 결과로 향할 수 있었다.

@Transactional X Lazy loading

Unable to evaluate the expression Method threw 'org.hibernate.LazyInitializationException' ~
'org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role' ~~

public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "ORDER_ID")
    private Long id;

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "RESTAURANT_ID")
    private Restaurant restaurant;

    @OneToMany(fetch = LAZY, mappedBy = "order")
    List<OrderFood> OrderFoods = new ArrayList<>();

    public Order(Restaurant restaurant) {
        this.restaurant = restaurant;
    }
}
  • 두번째 만난 문제이다. 리포지토리를 이용해 'Order'를 가져오고 그 객체를 이용해 위의 'OrderFoods'를 얻어오려 할 때 발생하는 문제이다. 에러는 위에 보이는 메시지를 통해 확인할 수 있다.
  • '실무에서는 fetch = 'LAZY'만을 사용해야 한다' 즉시, 지연 로딩 중에서 지연 로딩을 사용해야 한다는 얘기는 들어본 기억이 나지만 왜 그런지 알지 못하고 연관관계 매핑 시, 기계적으로 fetch = 'LAZY' 만을 주구장창 때려넣었었다.
  • 지연 로딩 시에는 연관관계에 있는 객체를 가져오려면 'Order'를 가져오는 영속성 컨텍스트와 동일한 곳에서 'OrderFoods'를 가져와야 한다. 하지만 'Order'를 가져온 뒤, 즉시 영속성 컨텍스트가 종료되어서 해당 Exception이 터지게 되는 것이였다.
  • 해당 예외는 테스트 메서드 레벨에 @Transactional 어노테이션을 작성하므로써 해결할 수 있었다. 다만 위 테스트 코드의 //when 시에 요청 데이터가 DB에 반영될 수 있게 EntityManager를 통해서 em.flush(), em.clear()를 호출해야 했다.
profile
step by step

0개의 댓글