안녕하세요. 이번 시간에는 테스트 코드를 작성하며 겪었던 문제에 대해 공유하고자 합니다.

image

테스트 코드 작성 시 테스트 간 고립이 되는지 고려해야합니다. 따라서 테스트를 위해 데이터를 넣었다면 해당 데이터를 정리해주는 작업이 필요합니다. 일반적으로 @Transactional을 활용해 롤백 테스트를 작성하거나 데이터를 넣은 뒤 afterEach 메서드를 활용해 일일이 데이터를 제거해 주는 방법을 사용합니다.

어떤 방법이 더 좋은 방법인지 의견 차이가 조금씩 있는 것 같습니다. @Transactional롤백 테스트 작성 시 주의해야할 점은 아래 링크를 참고해 주세요.


‘디클’ 프로젝트에서는 @Transactional을 통해 테스트 시작 시 트랜잭션을 시작하고 Kotest 라이브러리에서 경계 설정을 위해 제공하는 extension 메서드를 활용했습니다.


image

테스트에서 트랜잭션 경계를 시작하면 애플리케이션 코드, 즉 컨트롤러나 서비스 메서드를 호출하면 그 안에 존재하는 트랜잭션을 테스트 트랜잭션에 포함시키게 됩니다. 그 이유는 @Transactional의 기본 설정이 PROPAGATION_REQUIRED이기 때문입니다. 따라서 테스트가 끝날 때 전체 트랜잭션을 커밋 여부를 테스트 코드에서 정할 수 있습니다.


image

이제 예전에 작성했던 테스트 코드를 살펴보겠습니다. 코드의 흐름은 다음과 같습니다

  • Post 엔티티 생성 및 저장
  • Post 엔티티 제거
  • 데이터베이스에서 유저가 생성한 Post 엔티티 조회

위 코드를 실행하면 통과합니다. 하지만 이는 테스트 코드가 어떻게 동작하는지 이해하지 못한 채 운이 좋아 통과했을 뿐입니다. 통합 테스트를 작성할 때는 실제 데이터베이스까지 쿼리가 가야합니다. 그래야 배포 했을 때도 올바른 동작을 할 것이라 기대할 수 있기 때문입니다. 왜 올바르게 동작하는지 살펴보도록 하겠습니다.


image

첫 번째로 살펴봐야할 점은 save메서드 입니다. 현재 진행한 프로젝트에서는 MySQL을 사용했습니다. 키 생성 전략은 @GeneratedValue(strategy = GenerationType.IDENTITY)입니다. 이 전략은 INSERT 시 쓰기 지연이 동작하지 않으므로 즉시 데이터베이스에 저장 됨을 알고 있어야합니다. 만약 Oracle을 사용하는 데이터베이스에서 시퀀스를 키 생성 전략으로 사용했다면 명시적으로 flush를 호출해야 데이터베이스에서 조회가 가능합니다.

두 번째로 살펴봐야할 점은 findByUserId(user.id) 입니다. 여기서 user.idPost 엔티티의 id가 아닙니다. JPA에서는 id를 통한 조회가 아니라면 영속성 컨텍스트의 1차 캐시를 확인하지 않고 데이터베이스에 쿼리를 날립니다. 이 과정에서 영속성 컨텍스트의 쓰기 지연 저장소 내용 중 관련 있는 쿼리만 flush 합니다. 만약 postRepository.findById(post.id)를 호출한다면 실제 데이터베이스에 쿼리를 날리는 대신 1차 캐시를 기반으로 작업을 하게 됩니다. 따라서 통합 테스트의 의미가 퇴색되는 위험이 있습니다.

따라서 테스트 코드를 작성할 때는 위 내용들을 고려해서 작성해야 합니다. 특히 INSERT 이후 조회하는 쿼리를 작성할 경우, 실제로 데이터베이스에 저장이 되는지, 조회 시 데이터베이스에서 데이터를 가져오는지 명확히 파악해야 합니다. EntityManager#flush()를 명시적으로 호출하거나 jpql 혹은 findAll()을 통해 발생할 수 있는 문제를 예방해야 합니다. 팀 내에서 어떤 방식으로 테스트 코드를 작성할 것인지 정하고 일관된 코드 작성을 하면 좋을 것 같습니다

참고