도도한 개발자
[카페인] Repository 테스트 격리가 안돼요(이제 돼요) 본문
🐈⬛ 무슨일이야?
우아한테크코스 레벨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값까지 초기화되는 것은 아니다!
'우아한테크코스' 카테고리의 다른 글
[@MVC 미션] @Controller(value) vs @RequestMapping(value) (0) | 2023.09.14 |
---|---|
[우아한테크코스] 레벨3 레벨 인터뷰 회고 (1) | 2023.08.30 |
누가 도메인 테스트에서 Fixture를 사용하였는가 (0) | 2023.08.12 |