프리코스 예제 테스트 3/4 해결법

Kim Dong Kyun·2024년 11월 12일
6

이번 마지막 주차 구현은 3/4 문제로 프리코스 커뮤니티가 좀 뜨거웠다. 해당 문제는 '로컬에서는 ApplicationTest가 잘 도는데, 예제 테스트에서는 하나가 실패' 라는 증상이었다. 나도 해당 문제를 겪었고, 해결했다.

해결 과정을 코드로 살펴보자


1. 문제가 되는 테스트코드

	@Test
    void 파일에_있는_상품_목록_출력() {
        assertSimpleTest(() -> {
            run("[물-1]", "N", "N");
            assertThat(output()).contains(
                    "- 콜라 1,000원 10개 탄산2+1",
                    "- 콜라 1,000원 10개",
                    "- 사이다 1,000원 8개 탄산2+1",
                    "- 사이다 1,000원 7개",
                    "- 오렌지주스 1,800원 9개 MD추천상품",
                    "- 오렌지주스 1,800원 재고 없음",
                    "- 탄산수 1,200원 5개 탄산2+1",
                    "- 탄산수 1,200원 재고 없음",
                    "- 물 500원 10개",
                    "- 비타민워터 1,500원 6개",
                    "- 감자칩 1,500원 5개 반짝할인",
                    "- 감자칩 1,500원 5개",
                    "- 초코바 1,200원 5개 MD추천상품",
                    "- 초코바 1,200원 5개",
                    "- 에너지바 2,000원 5개",
                    "- 정식도시락 6,400원 8개",
                    "- 컵라면 1,700원 1개 MD추천상품",
                    "- 컵라면 1,700원 10개"
            );
        });
    }
    
   ...
   
// 아래는 데이터
name,price,quantity,promotion
콜라,1000,10,탄산2+1
콜라,1000,10,null
사이다,1000,8,탄산2+1
사이다,1000,7,null
오렌지주스,1800,9,MD추천상품
탄산수,1200,5,탄산2+1,500,10,null
비타민워터,1500,6,null
감자칩,1500,5,반짝할인
감자칩,1500,5,null
초코바,1200,5,MD추천상품
초코바,1200,5,null
에너지바,2000,5,null
정식도시락,6400,8,null
컵라면,1700,1,MD추천상품
컵라면,1700,10,null

여기서 무엇이 이상할까? 문제가 되는 부분은

"- 오렌지주스 1,800원 9개 MD추천상품",
"- 오렌지주스 1,800원 재고 없음",

오렌지주스,1800,9,MD추천상품
탄산수,1200,5,탄산2+1

부분이다. 즉, 프로모션 상품만 존재하고 일반 상품이 없으면 테스트가 통과하지 않는다.

나를 포함한 대부분의 분들은 간단하게 products.md 파일을 고쳐서 해결하셨을 것 같다. 왜냐면 요구사항에 "두 파일 모두 내용의 형식을 유지한다면 값은 수정할 수 있다."는 문장이 존재하기 때문이다.

하지만 이 문장의 의미는 "레코드의 값을 변조 해도 좋다(예를 들어 오렌지쥬스의 수량을 변경한다던지)" 이지, "레코드를 추가해도 좋다" 가 아니었다고 해석된다 (테스트코드의 동작을 확인하면)

더불어 리모트 테스트(예제 테스트)가 통과하지 않는 모습을 보면, 예제 테스트는 무조건 products.md 파일의 초기 모습으로 수행된다는 것을 캐치했다. (캐싱을 위해 resource 내부의 자원을 고정하고 사용했던지, 아니면 뭐 다른 이유가 있을 것 같다)

그러면 어떻게 고쳐야 할까?


2. 해결하기

 public List<Product> loadFileProducts() {
        return new ArrayList<>(load(FILE_PATH));
    }

    public List<Product> loadProducts() {
        List<Product> products = loadFileProducts();
        List<Product> promotionProducts = products.stream().filter(Product::promotionNotNull).toList();

        promotionProducts.stream()
                .filter(each -> products.stream().noneMatch(all -> isPromotionExistsAndNormalDont(each, all)))
                .map(each -> new Product(each.getName(), each.getPrice(), 0, null))
                .forEach(products::add);

        return List.copyOf(products);
    }
    
    private static boolean isPromotionExistsAndNormalDont(Product each, Product all) {
        return all.getName().equals(each.getName()) && !all.promotionNotNull();
    }

해당 코드는 파일 패스에 있는 데이터를 Product 인스턴스로 만들어서 초기화 하는 코드이다. 여기에서 promotion 이 있는 재고를 먼저 추출 한 뒤, "프로모션이 있지만 일반 상품이 존재하지 않는" 상품을 리스트화 해서 새로운 일반 상품으로 만든다 (같은 이름, 가격을 가지지만 재고는 0이고 프로모션도 null이다)

해당 방식으로 리모트 테스트를 통과 가능하다.


결론

나는 작성된 테스트코드에서 문제가 될 수 있는 부분을 찾아보고, 몇 가지 가설을 세웠다.

  1. 아마도, 제작자는 모든 사람이 일괄된 환경에서 테스트를 수행하게 하기 위해 테스트 케이스를 작성했을 것이다.
  2. 지금까지 미션을 수행하면서 로컬, 리모트에서 다른 결과를 낸 적이 없다.
  3. 따라서, 내가 제출한 코드 그 자체가 아닌 어떤 '테스트 데이터'를 사용해서 리모트의 예제 테스트를 수행하고 있을 것이다.
  4. 테스트 데이터를 발제자 임의로 만드는 것 보다는, '기본 데이터'(모든 사람에게 동일하게 주어진)로 예제 테스트를 수행하도록 설계되었을 것이다.

이에 따라 3/4 문제는 아마도 내가 products.md 파일을 수정해서 생긴 문제라는 것을 추측했다. 기능 요구의 " 두 파일 모두 내용의 형식을 유지한다면 값은 수정할 수 있다."는 문장의 해석 차이로 인해 발생한 착각이었다. 기획자의 의도는 새 행을 추가하라는 것이 아닌, 이미 존재하는 레코드의 값만 변조하라는 것이라고 이해했다.

재미있는 문제였다. 숨겨진 의도를 파악 한 것 같아 뿌듯하기도 하다.

2개의 댓글

comment-user-thumbnail
2024년 11월 12일

이거였군요! 만약 데이터가 존재하지않으면 임의의 mock데이터를 생성하는 느낌이군요

답글 달기
comment-user-thumbnail
2024년 11월 12일

오호 3/4클럽을 만든 게 md파일이었군요..!
저는 로컬에서 테스트했을 때 저 데이터가 없다는 경고를 보고 요구사항적 이슈라고 판단했는데 운이 좋았던 것 같네요.. ㄷㄷ
깊은 통찰력과 해결과정 정리 후 공유까지 감탄하고 갑니다!
프리코스 고생하셨습니다!!

답글 달기