도도한 개발자

[카페인] Repository 테스트 격리가 안돼요(이제 돼요) 본문

우아한테크코스

[카페인] Repository 테스트 격리가 안돼요(이제 돼요)

Kiara Kim 2023. 9. 1. 23:05

🐈‍⬛ 무슨일이야?

우아한테크코스 레벨4에 접어들면서 레벨3때 진행했던 프로젝트 리팩터링이 시작됐습니다. 저희 팀은 제일 먼저 조회 기능을 Querydsl로 변경하는 작업을 첫 번째로 삼았습니다. 그래서 어제부터 시작했고 문제는 테스트 코드를 작성하면서 일어났습니다.

 

@Test
void 리뷰_13개_중_첫번째_페이지엔_10개의_리뷰가_있다() {
    // given
    for (Review review : 리뷰_13개(member)) {
        reviewRepository.save(review);
    }
    // 생략

    // when
    Page<ReviewResponse> allReviews = reviewQueryRepository.findAllReviews(station.getStationId(), pageable);
    ReviewResponse expected = new ReviewResponse(13L, "생략");

    // then
    assertThat(allReviews.getContent().get(0))
        .usingRecursiveComparison()
        .ignoringFieldsOfTypes(LocalDateTime.class)
        .isEqualTo(expected);
}

제가 확인하고자 한 것은 13개의 review가 save된 후 reviewQueryRepository에서 paging 처리된 최근 10개의 리뷰 중 제일 첫 번째(최신 리뷰) 리뷰의 id가 13L인지였습니다.

 

 

 

 

 

묶어놓은 10개의 리뷰는 paging 처리되어 응답으로 갈 리뷰들입니다.

 

리뷰가 하나 save 될 때마다 id의 값이 증가되는데 본 테스트에선 13번의 save를 했으니 맨 마지막 리뷰가 13L이라고 예상할 수 있습니다.

 

이 테스트를 돌리면 성공합니다.

그럼 이 클래스 테스트를 돌려도 성공해야겠죠?

 

 

 

 

 

 

 

39L? 뭔가 잘못된 것 같아요. 그치만 다행히 39L이 어디서 나온건진 알 것 같습니다.

 

이 테스트들이 한 클래스에 있는 테스트들인데 두번째 테스트를 제외하고 앞 두 테스트에서 저장된 리뷰들이 롤백되지 않은 것이 원인이네요. 

 

근데 왜 롤백이 안될까요?

 

@DataJpaTest 어노테이션을 붙여줬으면 이 안에 있는 @Transactionl 어노테이션도 같이 가져와 롤백이 되어야 할텐데 말이죠.

뭔가 액션이 필요해보입니다.

🐾 각 테스트가 끝날 때마다 테이블을 TRUNCATE 하기

@AfterEach
void afterEach(){
    entityManager.createNativeQuery("TRUNCATE TABLE review;").executeUpdate();
}

안됩니다.

🐾 예상 값의 id 무시하기

assertThat(allReviews.getContent().get(0))
    .usingRecursiveComparison()
    .ignoringFieldsOfTypes(LocalDateTime.class)
    .ignoringFields("reviewId")
    .isEqualTo(expected);

되긴 되는데... 사실 이렇게 해서 성공이 됐었습니다. 이대로 PR을 날렸는데 id를 ignoring 하는 부분이 어색하다는 코드리뷰를 받아 id까지 검증하려고 하는 것입니다.

 

아무리 머리를 굴려도 해결이 안돼서 박스터에게 도움을 요청했습니다. 

🐾 최근 10개의 리뷰를 가져와 비교하기

for (Review review : 리뷰_13개(member)) {
    reviewRepository.save(review);
}

기존에 위와 같은 for문으로 리뷰 13개를 저장하는 방법 대신

 

List<Review> reviewList = 리뷰_13개(member).stream()
    .map(it -> reviewRepository.save(it))
    .toList();

이런 식으로 리뷰들을 가져옵니다.

 

그러고 나서 예상 값을

// ReviewResponse expected = new ReviewResponse(13L, "생략");

List<ReviewResponse> expected = reviewList.stream()
                .sorted(Comparator.comparing(Review::getId).reversed())
                .limit(10)
                .map(it -> new ReviewResponse(it.getId(), it.getMember().getId(), it.getUpdatedAt(), it.getRatings(), it.getContent(), it.getUpdatedAt().isAfter(it.getCreatedAt()), it.isDeleted(), 0))
                .toList();

이런식으로 바꾸면 테스트가 성공하게 됩니다.

 

💭 그럼 격리가 된건가?

그건 아닙니다. 여전히 최신 리뷰의 id는 39L이죠...ㅠ

 

어떻게 해야 저 39 라는 id가 13이 되는 모습을 볼 수 있을까요? 여기서 타협을 하기로 했습니다. (합리화 아님)

 

save를 하고 반환되는 13개의 리뷰들 중 최근 10개의 리뷰만 가져온 것을 예상 값으로 잡았습니다. 

그리고 나서 전체 리뷰들 중에서 페이징 처리로 최근 리뷰 10개만 갖고 온 것들이 실제 값입니다.

이 둘이 같음을 검증하는 것도 충분히 의미가 있다고 생각합니다.

 

레벨4의 우선순위는 미션으로 가지고 가야 합니다. 그렇지만 프로젝트는 여전히 중요하고 그렇기 때문에 시간 분배를 잘 해야합니다. 왜 테스트 격리가 잘 안되는지 너무나도 궁금하지만 지금으로서는 이게 최선이라고 생각합니다. 

 

테스트를 잘 짜고 싶네요 흑흑... 지금까지 테스트를 잘 못짜서 슬퍼하는 글이었습니다. 감사합니다.

 


띠링~

아...! truncate는 pk를 초기화 하는게 아니었습니다. 알고 있었는데 실수한 거리면 차라리 전 몰랐던 겁니다.

@AfterEach
void afterEach() {
    entityManager.createNativeQuery("ALTER TABLE review AUTO_INCREMENT=1").executeUpdate();
    entityManager.createNativeQuery("SET @COUNT=0").executeUpdate();
    entityManager.createNativeQuery("UPDATE review SET id=@COUNT:=@COUNT+1").executeUpdate();
}

이런 방식도 있었지만 제가 잘 이해를 못한건지 다음과 같은 에러가 났습니다.

 

javax.persistence.PersistenceException: org.hibernate.exception.SQLGrammarException: could not prepare statement

...

Caused by: org.h2.jdbc.JdbcSQLSyntaxErrorException: Syntax error in SQL statement "ALTER TABLE review [*]AUTO_INCREMENT=1"; expected "., ADD, SET, RENAME, DROP, ALTER"; SQL statement:
ALTER TABLE review AUTO_INCREMENT=1 [42001-214]

 

다른 방법을 찾아보다 한 줄로 해결할 수 있는 방법이 있어 다음과 같이 수정해줬습니다.

@AfterEach
void afterEach() {
    entityManager.createNativeQuery("ALTER TABLE review ALTER COLUMN id RESTART WITH 1").executeUpdate();
}

 

그랬더니 성공...!!

예상 값도 제가 예상한 것 처럼 나왔습니다.

 

진짜 주노 너무 고마워ㅠㅠ

주노가 말해주기 전에 힌트를 준 주드도 고마워🥲

 

오늘의 교훈: truncate는 테이블의 데이터를 전부 삭제할 뿐 auto_increment로 생성되는 pk값까지 초기화되는 것은 아니다!