[JUnit] 중첩 클래스로 테스트 코드 작성하기

jina·2024년 10월 8일
0

Java

목록 보기
10/11

사용하려는 데이터베이스에 액세스가 가능한지, CRUD 기능이 정상적으로 동작하기 위해 테스트 코드에 중첩 클래스를 사용하면, 비즈니스 로직과 별도로 저장소 인터페이스의 작동과 관련된 기능에 집중해 테스트가 가능합니다.

테스트 계층 코드는 아래와 같습니다.

  • JpaRepository
    • InMemoryDBTest (중첩 테스트 클래스)
    • ActualDBTest (중첩 테스트 클래스)
    • DBTest (추상 클래스)

이 테스트 코드는 세 가지 내부 클래스를 가지고 있습니다. 인메모리 DB인 H2를 사용하는 테스트, 실제 DB인 SQL을 사용하는 테스트, 그리고 DBTest라는 추상 클래스입니다. 실제 DB에서도 제대로 작동하고 있는지 눈으로 확인할 수 있도록 테스트를 두 가지로 나눴지만, 두 테스트가 정확히 동일한 로직으로 테스트를 실행해야 한다는 의도를 명확히 하기 위해 추상 클래스를 추가로 두었고, 두 테스트가 이 추상 클래스를 확장해서 사용하도록 구현된 모습입니다.


@DisplayName("[DB] JPA 연결 테스트")
class JpaRepositoryTest {

    @Nested
    @DisplayName("인-메모리 DB 테스트")
    class InMemoryDBTest extends DBTest {

        public InMemoryDBTest(
                @Autowired FirstRepository firstRepository,
                @Autowired SecondRepository secondRepository
        ) {
            super(firstRepository, secondRepository);
        }

    }

    @Nested
    @DisplayName("실제 DB 테스트")
    @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
    class ActualDBTest extends DBTest {

        public ActualDBTest(
                @Autowired FirtstRepository firstRepository,
                @Autowired SecondRepository secondRepository
        ) {
            super(firstRepository, secondRepository);
        }

    }


    @Import(IfConfigNecessary.class)
    @DataJpaTest
    private abstract class DBTest {

        private final FirstRepository firstRepository;
        private final SecondRepository secondRepository;

        public DBTest(
                FirstRepository firstRepository,
                SecondRepository secondRepository
        ) {
            this.firstRepository = firstRepository;
            this.secondRepository = secondRepository;
        }

        @DisplayName("select 테스트")
        @Test
        void givenTestData_whenSelecting_thenWorksFine() {
            // Given & When & Then logic...
        }

        @DisplayName("insert 테스트")
        @Test
        void givenTestData_whenInserting_thenWorksFine() {
            // Given & When & Then logic...
        }

        @DisplayName("update 테스트")
        @Test
        void givenTestData_whenUpdating_thenWorksFine() {
            // Given & When & Then logic...
        }

        @DisplayName("delete 테스트")
        @Test
        void givenTestData_whenDeleting_thenWorksFine() {
            // Given & When & Then logic...
        }

    }

}

Nested로 중첩 클래스 만들기

이 구조에서 눈여겨 볼 기능은 JUnit5가 제공하는 @Test와 @Nested인데요. 특히, @Neseted로 중첩 클래스를 설정하는 기능 덕분에 두 테스트가 공통 테스트 로직을 공유할 수 있게 되어서, 이 테스트의 의도가 코드에 잘 반영되었습니다. Nested 어노테이션의 내부 코드를 확인하면, 이 어노테이션을 사용한 비정적 중첩 내부 클래스는 상위 테스트 클래스와 설정과 상태를 공유할 수 있다는 것을 알 수 있습니다.

상속할 추상 클래스 작성하기

위 코드에서는 Neseted 기능을 사용한 두 중첩 테스트가 상속하는 추상 클래스에 CRUD 테스트 메소드가 작성되었습니다.

💡 update 테스트에서 중간 저장을 할 때

save와 saveAndFlush를 사용할 때, 두 메서드는 저장하는 동작을 하지만 트랜잭션 커밋 시점에 차이가 있습니다.

아래 코드는 게시판 서비스에서 게시글의 해시태그 수정을 테스트하는 예시입니다. 업데이트 테스트의 실행(when) 부분에서 데이터를 중간 저장하며 saveAndFlush를 사용하고 있습니다.

@DisplayName("update 테스트")
        @Test
        void givenTestData_whenUpdating_thenWorksFine() {
            // Given
            Article article = articleRepository.findById(1L).orElseThrow();
            article.setHashtag(updatedHashtag);

            // When
            Article savedArticle = articleRepository.saveAndFlush(article);

            // Then
            assertThat(savedArticle).hasFieldOrPropertyWithValue("hashtag", updatedHashtag);
        }

save와 saveAndFlush는 데이터가 언제 데이터베이스에 반영되어야 하는지에 따라 사용합니다. 즉시 데이터베이스에 반영이 필요하지 않고 트랜잭션이 끝났을 때 자동으로 반영되어도 괜찮다면 save로 충분하고, 즉시 데이터베이스에 변경이 필요하다면 flush가 추가로 필요합니다.

데이터베이스에 변경이 일어나는 시점은 트랜잭션이 끝난 후입니다. 이 코드에서는 업데이트 테스트 메소드가 트랜잭션으로 실행되고 있습니다. 예제 코드에서 볼 수 있듯이 @DataJpaTest 어노테이션이 설정되어 있어 아래와 같은 내용이 자동으로 실행됩니다.

  • JPA 관련 설정만 로드
  • 각 테스트 메소드에 트랜잭션 적용
  • 테스트가 끝나면 DB를 롤백

예시 코드에서는 두 방식 중 어느 것을 택하더라도 결과적으로 테스트가 성공합니다.

Hibernate는 영속성 컨텍스트로 엔티티를 관리합니다. save 호출 시 엔티티가 영속성 컨텍스트에 저장되기 때문에, Hibernate는 DB 대신 영속성 컨텍스트에 캐싱된 내용을 참고하게 됩니다. 따라서 save만 사용하고 flush를 호출하지 않아도, 테스트 코드의 then 절에서 결과를 검증할 수 있습니다. 하지만, 트랜잭션이 끝나야 자동 커밋이 실행되기 때문에 실제 데이터베이스에는 반영되지 않은 채 비교됐다는 것을 알 수 있습니다.

// given: 엔티티 저장
entityRepository.save(entity);

// then: flush가 호출되지 않았지만, 영속성 컨텍스트에 의해 엔티티 상태는 업데이트됨
assertThat(entity.getSomeField()).isEqualTo(expectedValue);

한편, saveAndFlush를 사용하면 hibernate가 변경 사항을 데이터베이스에 즉시 반영합니다. 콘솔에서 로그를 확인해보면 Hibernate가 객체 변경사항을 감지해서 SQL UPDATE 쿼리를 실행하는 것을 확인할 수 있습니다. 이 경우 테스트 코드에서 결과를 검증할 때 DB 상태를 기준으로 로직이 실행됩니다. (콘솔에 SQL문이 보이지 않는다면 spring.jpa.show-sql: true 설정을 추가해 주세요)

테스트 코드의 의도에 따라 변경 사항을 데이터베이스에 바로 반영해야 하는 경우인지를 고려해서 save와 saveAndFlush를 사용하는 것이 중요하겠습니다.


참고
https://jojoldu.tistory.com/415 (dirty checking이란)

profile
오늘의 기록은 내일의 보물

0개의 댓글