Spring & Spring boot

[Springboot] 테스트 코드 - 8 - Repository Unit Test 리포지토리 단위 테스트

jh4dev 2024. 8. 8. 10:45
본 포스팅은, 단위 테스트 (Unit Test) 와 관련된 내용입니다.
작성된 내용 외에도 다양한 방법이 있으며, 정답은 없습니다.
<목차>

1. 리포지토리 단위 테스트
2. 테스트 코드
3. GitHub

 

SpringBoot 환경에서 전체적인 비즈니스 로직이 정상적으로 동작하는지 테스트 하기 위해서는 통합 테스트를 해야 하고, 각 모듈을 테스트 하고 싶다면, 단위 테스트를 진행해야 한다.

일반적인 Spring MVC 를 사용하는 구조라면, 일부 레이어에서만 단위 테스트를 수행하는게 무의미하거나 어려운 경우가 많다.

하지만! Mock 객체를 활용하여, 하위 레이어와 무관하게 동작하도록, 레이어 별 단위 테스트 코드를 작성해보도록 하겠다. 

 

앞서 JPA Example 에서 작성한 소스를 기반으로 작성된 포스팅이며,
Controller - Service - DAO - Repository 구조에서, DAO 를 제외한 구조로 재구성 하였다.

2024.08.04 - [Programming/Spring & Spring boot] - [Spring Boot] JPA - 4. CRUD Example

 

[Spring Boot] JPA - 4. CRUD Example

1. Environment2. Sources3. Test Springboot - Basic JPA CRUD API Example JPA를 바로 사용하기 보단, 동작 원리에 대해 알고 사용하는 것이 도움이 된다.JPA 관련 포스팅https://jh4dev.tistory.com/24 [Spring Boot] JPA - 1. ORM

jh4dev.tistory.com


[리포지토리 단위 테스트]

Repository는 구현되는 레이어 중, 가장 데이터베이스와 가까운 레이어이다.

하지만 데이터베이스를 외부 요인으로 볼 수 있기 때문에, 단위 테스트 시 데이터베이스를 연동하는 부분을 포함시키지 않는 경우도 있다.

 

하지만, 데이터베이스 연동 부분을 제외한다면 Repository를 테스트 하는 의미가 감소하기 때문에, 연동되는 부분을 테스트하는 방법에 대해 알아보자.

 

Repository 테스트에서 고려해야하는 내용은 다음과 같다.

  • 데이터베이스를 사용하는 테스트는 테스트 과정에서 실제 데이터베이스에 데이터가 적재되므로, 아래와 같은 방법을 통하여 테스트를 진행한다.
    • 별도의 테스트용 데이터베이스를 사용한다.
    • 테스트로 생성된 데이터를 제거하는 코드까지 포함하여 작성한다.
  • 위 내용과 관련하여 다음과 같이 Repository 테스트 환경을 구성하는 부분을 중점적으로 다루겠다.
    • Embeded DB 사용 ( H2 DB )
    • 실제 개발 환경 DB 사용 ( MariaDB )
      • @JpaDataTest 어노테이션 사용
      • @SpringBootTest 어노테이션 사용

[테스트 코드]

테스트를 진행할 Repository 는 다음과 같다.

    import com.springboot.test.data.entity.Product;
    import org.springframework.data.jpa.repository.JpaRepository;

    public interface ProductRepository extends JpaRepository<Product, Long> {
    }

 

테스트 진행할 ProductRepository 를 살펴보자.

  • Entity
    • Product Entity 사용
  • JpaRepository 상속
    • 별도의 커스텀 메서드 없이, 기본 메서드로 테스트 코드 작성하는 방법에 대해 알아볼 예정이다.
    • 실제 개발 환경에서, JpaRepository 에서 제공되는 기본 메서드를 테스트하는 것은 이미 검증이 완료된 메서드기 때문에 무의미한 부분이 있으나, 데이터베이스 연동 부분과 관련된 테스트 환경에 대해 작성하겠다.

<Embeded DB 사용>

우선, Embeded DB인 H2 DB 를 사용하기 위한 의존성 추가부터 진행한다.

    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>test</scope>
    </dependency>

 

아래는, H2 DB를 사용하는 ProductRepository 테스트 코드이다.

    @DataJpaTest
    public class ProductRepositoryTestByH2 {

        @Autowired
        private ProductRepository productRepository;

        //저장 테스트
        //테스트에 h2 를 사용한다면, dialect 설정 유의
        @Test
        public void saveTest() {

            //Given
            Product product = new Product();
            product.setName("연필");
            product.setPrice(500000);
            product.setStock(2020);

            //When
            Product savedProduct = productRepository.save(product);

            //Then
            Assertions.assertEquals(product.getName(), savedProduct.getName());
            Assertions.assertEquals(product.getPrice(), savedProduct.getPrice());
            Assertions.assertEquals(product.getStock(), savedProduct.getStock());
        }

        @Test
        void selectTest() {

            //Given
            Product product = new Product();
            product.setName("연필");
            product.setPrice(500000);
            product.setStock(2020);

            Product savedProduct = productRepository.saveAndFlush(product);

            //When
            Product foundProduct = productRepository.findById(savedProduct.getNumber()).get();

            //Then
            Assertions.assertEquals(product.getName(), foundProduct.getName());
            Assertions.assertEquals(product.getPrice(), foundProduct.getPrice());
            Assertions.assertEquals(product.getStock(), foundProduct.getStock());
        }
    }

 

작성된 테스트 코드에 대해 하나씩 살펴보자.

  1. @DataJpaTest
    • JPA 와 관련된 설정만 로드하여 테스트를 진행한다.
    • 내부적으로 @Transactional 어노테이션을 포함하고 있어, 테스트 코드가 종료되면 자동으로 DB Rollback 이 진행된다.
      위에서 언급한, 생성된 데이터를 제거하는 내용은 해당 어노테이션을 사용함으로써 해결할 수 있다.
    • Default 로는 Embeded 데이터베이스를 사용하며, 실제 개발 환경에서 사용하는 데이터베이스로 테스트하기 위해서는 별도의 설정을 해주어야 한다.
      별도로 설정하는 방법은 다음 예제를 통해 알아보자.
    • selectTest() 테스트 메서드 중간을 보면, saveAndFlush() 메서드를 사용하고 있다. (36번 라인)
      @DataJpaTest 어노테이션을 사용한 테스트이므로, "product" 테이블은 비어있는 상태이며, saveAndFlush() 메서드를 통해 조회해올 데이터를 미리 테이블에 넣어놓는다.
    • saveAndFlush() 메서드는 Entity를 저장하고, 영속성 컨텍스트의 변경사항을 데이터베이스에 즉각 반영한다.
      그러나, 트랜잭션을 커밋하는 것은 아니며, 롤백이 발생하는 경우 해당 데이터도 롤백된다.
      즉,  조회해오기 위해 테이블에 임시(?)적으로 적재시키는 메서드이다. 
  2. Given - When - Then 패턴
    • selectTest() 메서드로 설명하겠다.
    • Given
      • 30번 라인
        findById() 메서드를 통해 조회할 Product Entity 를 정의한다.
    • When
      • 38번 라인
        findById() 메서드 테스트 진행
    • Then
      • 41번 라인 
      • When 단계에서 리턴받은 Product Entity 객체 (foundProduct) 에 대한 값을 검증한다.
        Jupiter Assertions 을 사용하여 리턴된 결과를 검증
  3. DB dialect 설정
    • dialect 는 JPA와 데이터베이스 간의 일관성을 유지하기 위해 사용하는 설정이다.

만약, dialect 를 실제 사용하는 DB로 설정하였다면 오류가 발생한다.
개발 환경은 MariaDB 로 구성되어 있으나, H2 를 사용하려 하니, 문법이 맞지 않아 발생하는 오류이며, 이를 해결하기 위해 테스트용 application.properties 를 따로 구성하기도 한다.

=오류 예시=

MariaDB dialect 설정으로 인하여, H2 사용하도록 설정된 경우 오류 에러 발생


<application-test.properties 파일로 테스트 환경 구축하기>

위에서 언급한, 테스트용 application.properties 를 구성해보자.

  1. test 디렉토리 내,  application-test.properties 파일을 생성한다.
    application-test.properties 생성
  2. 테스트 시, 사용할 애플리케이션 설정 정보를 작성한다.
        # H2 DB 설정
        spring.datasource.url=jdbc:h2:mem:testdb
        spring.datasource.driver-class-name=org.h2.Driver
        spring.datasource.username=sa
        spring.datasource.password=password
        spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
    
        spring.jpa.hibernate.ddl-auto=create
        spring.jpa.show-sql=true
        spring.jpa.properties.hibernate.format_sql=true
  3. Test 코드에 @ActiveProfilies 어노테이션을 활용해 application-test.properties 파일을 바라보도록 추가
        @DataJpaTest
        @ActiveProfiles("test")    //application-test.properties 를 사용하도록 설정
        public class ProductRepositoryTestByH2 {
    
            @Autowired
            private ProductRepository productRepository;
    
            ... 이하 생략


  4. 실행 결과
    selectTest() 실행 결과

테스트를 실행한 로그를 자세히 살펴보자.

"test" 프로파일을 사용한다는 내용
Embeded DB 를 사용(H2 를 사용한다는 내용)
H2Dialect 를 사용하여, Product 테이블을 삭제하고 다시 생성

 

saveAndFlush() 메서드를 통해 Product 테이블 내 데이터 생성

 

테스트 종료 후 롤백

 

이와 같은 절차로 ProductRepository 가 테스트 된 것을 확인할 수 있다.

 


<Persistence Context 확인하기>

근데, 이상한 부분이 있다.

테이블을 생성하는 CREATE 쿼리와, saveAndFlush() 에서 실행되는 INSERT 쿼리는 모두 로그에 기재되었으나, findById() 메서드에 대한 로그가 찍히지 않은 것이다.

그렇다. saveAndFlush() 로 생성된 데이터가 Persistence Context (영속성 컨텍스트) 의 캐시에 저장되어 있기 때문에, DB를 조회하지 않는 것이다.

** 그렇다면 EntityManager 를 사용하여, 영속성 컨텍스트에 savedProduct 엔티티가 저장되어 있는지 확인해보자.

EntityManager 의존성을 주입하고,
Assertion 을 사하여, Persistence Context에 savedProduct 가 존재하는 경우 에러가 발생하게 하였다.

    @DataJpaTest
    @ActiveProfiles("test")
    public class ProductRepositoryTestByH2 {

        @Autowired
        private EntityManager entityManager; //EntityManager 의존성 주입

        @Test
        void selectTest() {

            //Given
            Product product = new Product();
            product.setName("연필");
            product.setPrice(500000);
            product.setStock(2020);

            Product savedProduct = productRepository.saveAndFlush(product);

            //When
            Product foundProduct = productRepository.findById(savedProduct.getNumber()).get();

            //Then
            //Assertion을 사용하여 Persistence Context에 savedProduct가 있는 경우 에러 발생하도록 작성
            Assertions.assertFalse(entityManager.contains(savedProduct), "Saved Product is in the Persistence Context");
            //Assertions.assertEquals(product.getName(), foundProduct.getName());
            //Assertions.assertEquals(product.getPrice(), foundProduct.getPrice());
            //Assertions.assertEquals(product.getStock(), foundProduct.getStock());
        }
    }

실행 결과

테스트 실행 결과, 위와 같이 savedProduct 가 영속성 컨텍스트에 존재하기 때문에 오류가 발생하는 것을 확인할 수 있다.

만약 로그로 select 되는 내용을 보고 싶다면, findById() 메서드를 호출하기 전에 entityManager 를 통해 영속성 컨텍스트를 클리어 해주면 된다.

    ... 생략 ...
    
    Product savedProduct = productRepository.saveAndFlush(product);

    //When
    entityManager.clear();
    Product foundProduct = productRepository.findById(savedProduct.getNumber()).get();
    
    //Then
    Assertions.assertFalse(entityManager.contains(savedProduct), "Saved Product is in the Persistence Context");
    ... 생략 ...

 

 

 

이처럼, 똑같은 Assert 구문이 작성되어 있으나, entityManager.clear() 를 통해 영속성 컨텍스트 캐시를 비웠기 때문에 findById() 에서 사용하는 SELECT 쿼리가 로그에 기재되는 것을 확인할 수 있다.

 


<실제 개발환경과 동일한 데이터베이스 사용>

이번엔, 실제 개발환경과 동일한 환경에서 테스트 할 수 있도록 구성해보자.

 

  1. @AutoConfigureTestDatabase 어노테이션 사용
    • @AutoConfigureTestDatabase는 Spring Boot의 통합 테스트에서 데이터베이스 설정을 자동으로 구성할 때 사용된다.
      앞서 설명했다시피, 별다른 설정이 없다면 빠르고 독립적으로 실행될 수 있도록 하기 위해 실제 데이터베이스를 사용하지 않고 임베디드 데이터베이스(H2, HSQL, Derby 등)를 사용한다. 
    • 이 어노테이션의 replace 속성으로 테스트 중에 사용할 데이터베이스를 어떻게 대체할 것인지 설정할 수 있다.
      1. Replace.ANY (Default)
        • Embeded 데이터베이스가 사용 가능하면, 애플리케이션의 실제 데이터베이스 설정을 임베디드 데이터베이스로 대체한다.
        • 앞서 작성한 예제에서는 H2 DB를 사용할 예정이였으므로, 해당 어노테이션을 따로 작성하지 않고 Default 옵션으로 사용한 것이다. 
      2. Replace.NONE
        • Spring Boot가 자동으로 데이터베이스 설정을 임베디드 데이터베이스로 대체하지 않도록 한다.
        • 이 설정을 사용하면 애플리케이션의 실제 데이터베이스 설정이 그대로 사용된다.
    • Source
          @DataJpaTest
          @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
          public class ProductRepositoryTestWithProjectConfig {
      
          ... 이하 생략
          }


  2. @SpringBootTest 어노테이션 사용
    • 주로 Spring Boot에서 통합 테스트를 수행할 때 사용되는 어노테이션으로, 실제 애플리케이션 컨텍스트를 로드하고 모든 빈을 초기화하여 전체 애플리케이션을 테스트할 수 있다.
    • 다만, 모든 스프링 설정을 가져오고, 모든 빈 객체를 스캔하기 때문에 단위 테스트에서는 적합하지 않을 수 있다.
    • Source
          @SpringBootTest
          public class ProductRepositoryTestWithSpringBootAnno {
      
          ...이하 생략
          }

 

8개의 포스팅에 걸쳐, Spring Boot 환경에서 Test 에 대해 알고 있으면 좋은 개념 / Framework / Library 등에 대해 알아봤으며, Controller - Service - Repository 구조에서 각각의 레이어에 대해 단위 테스트를 진행하는 방법을 작성하였다.

 

2024.08.04 - [Programming/Spring & Spring boot] - [Springboot] 테스트 코드 - 1 - 테스트 개요

2024.08.05 - [Programming/Spring & Spring boot] - [Springboot] 테스트 코드 - 2 - TDD 테스트 주도 개발이란?

2024.08.06 - [Programming/Spring & Spring boot] - [Springboot] 테스트 코드 - 3 - JUnit5

2024.08.06 - [Programming/Spring & Spring boot] - [Springboot] 테스트 코드 - 4 - Assertion 단정문

2024.08.06 - [Programming/Spring & Spring boot] - [Springboot] 테스트 코드 - 5 - Mockito

2024.08.07 - [Programming/Spring & Spring boot] - [Springboot] 테스트 코드 - 6 - Controller Unit Test 컨트롤러 단위 테스트

2024.08.07 - [Programming/Spring & Spring boot] - [Springboot] 테스트 코드 - 7 - Service Unit Test 서비스 단위 테스트

2024.08.08 - [Programming/Spring & Spring boot] - [Springboot] 테스트 코드 - 8 - Repository Unit Test 리포지토리 단위 테스트

 


[GitHub]

전체 소스
ProductRepository Test 소스

 

 

[참고 도서]

 

<스프링 부트 핵심 가이드>
저자 : 장정우

출판사 : 위키북스