본문 바로가기
개념 공부/Test Code

[Test Code / Java] Spring 프로젝트에서 Persistence Layer의 테스트

by clean01 2024. 12. 29.

Persistence Layer의 테스트

Persistence Layer는 데이터에 액세스하는 영역이다.

데이터베이스에 액세스하는 로직만 갖고 있기 때문에, 단위 테스트의 성격을 많이 띠고 있다.

Persistence Layer를 테스트하는 이유는 내가 작성한 코드로 인해 쿼리가 제대로 나갈 것인가를 검증하기 위함이다.

 


@SpringBootTest, @DataJpaTest

@SpringBootTest: 스프링에서 통합 테스트를 제공하는 어노테이션. 이 어노테이션을 달고 테스트하면 스프링 서버를 띄워서 테스트할 수 있다.

@DataJpaTest: 얘도 스프링 서버를 띄워서 테스트하는 어노테이션이다. 근데 이 어노테이션은 스프링부트 테스트보다는 가볍다. JPA 관련된 빈들만 띄워서 주입을 해주기 때문이다.

 


Persistence Layer 테스트 예시

카페 키오스크에서 음료를 주문할 수 있는데, 음료의 판매 상태에는 판매중(SELLING), 판매보류(HOLD), 판매중지(STOP_SELLING) 등이 있다.

아래 코드처럼 ProductRepository를 정의해주고, 그 안에는 findAllBySeelingStatusIn 이라고하는 음료의 판매 상태로 음료를 조회하는 쿼리 메서드를 정의해주었다.

@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
    List<Product> findAllBySellingStatusIn(List<ProductSellingType> sellingTypes);

}

이 쿼리 메서드가 제대로 정의 되었는지를 테스트하는 테스트 코드의 예시는 아래와 같다.

@ActiveProfiles("test")
// @SpringBootTest
@DataJpaTest
class ProductRepositoryTest {

    @Autowired
    private ProductRepository productRepository;

    @DisplayName("원하는 판매 상태를 가진 상품들을 조회한다.")
    @Test
    void findAllBySellingStatusIn() {
        // given
        Product product1 = Product.builder()
                .productNumber("001")
                .type(HANDMADE)
                .sellingStatus(SELLING)
                .name("아메리카노")
                .price(4000)
                .build();

        Product product2 = Product.builder()
                .productNumber("002")
                .type(HANDMADE)
                .sellingStatus(HOLD)
                .name("카페라떼")
                .price(4500)
                .build();

        Product product3 = Product.builder()
                .productNumber("003")
                .type(HANDMADE)
                .sellingStatus(STOP_SELLING)
                .name("팥빙수")
                .price(4500)
                .build();

        productRepository.saveAll(List.of(product1, product2, product3));

        // when
        List<Product> products = productRepository.findAllBySellingStatusIn(List.of(SELLING, HOLD));

        // then
        assertThat(products).hasSize(2)
                .extracting("productNumber", "name", "sellingStatus")
                .containsExactlyInAnyOrder(
                        tuple( "001", "아메리카노", SELLING),
                        tuple("002", "카페라떼", HOLD)
                );

    }

 

하나씩 살펴보자.

 

@ActiveProfiles("test")

해당 어노테이션을 통해, 해당 테스트가 실행될 때는 test 환경에서 실행하라는 의미이다.

이렇게 하면 application.yml의 내용 중에서 spring.config.active.on-profile 이 test에 해당하는 설정 값들을 가지고 테스트 코드를 실행하게 된다.

예를 들어 아래와 같은 application.yml이 있다면

spring:
  profiles:
    default: local

  datasource:
    url: jdbc:h2:mem:~/cafeKioskApplication;DB_CLOSE_DELAY=-1 # localhost:8080/h2-console로 접속
    driver-class-name: org.h2.Driver
    username: sa
    password:

  jpa:
    hibernate:
      ddl-auto: none # ddl auto 설정은 기본 프로파일에서는 none으로, 필요한 곳에서는 맞게 지정해주는 것이 좋다.

---
spring:
  config:
    activate:
      on-profile: local

  jpa:
    hibernate:
      ddl-auto: create
    show-sql: true
    properties:
      hibernate:
        format_sql: true # 쿼리 날라가는거 포맷팅. 더 알아보기 쉽게 볼 수 있음
    defer-datasource-initialization: true # 하이버네이트 2.5부터 존재. 하이버네이트 초기화 이후 data.sql 실행

  h2:
    console:
      enabled: true

---

spring:
  config:
    activate:
      on-profile: test
  jpa:
    hibernate:
      ddl-auto: create
    show-sql: true
    properties:
      hibernate:
        format_sql: true

  sql:
    init:
      mode: never # 테스트 시에는 내가 데이터를 만듦 따라서 sql.init.mode: never

 

가장 상단의

spring:
  profiles:
    default: local

이 부분 때문에, 아무 것도 지정해주지 않았다면 local 환경의 환경변수를 가지고 스프링부트 테스트(또는 JPA 테스트)가 실행되게 된다.

하지만 ProductRepositoryTest 클래스 위에 @ActiveProfiles(”test”)를 달아주었기에, application.yml 하단에 작성된 test 환경으로 스프링부트 테스트가 실행되게 된다.

 

given

given 부분에서는 3개의 product를 만들고, repository에 모두 저장해주었다.

productRepository.saveAll(List.of(product1, product2, product3));

 

when

when 부분에서는 ProductRepository에 정의된 쿼리 메서드인 findAllBySellingStatusIn 를 이용해서 판매 상태가 HOLD, SELLING 인 상품만 조회했다.

List<Product> products = productRepository.findAllBySellingStatusIn(List.of(SELLING, HOLD));

 

then

then 부분에서는 조회 결과 리스트인 products에 대한 여러 검증을 하고 있다.

        // then
        assertThat(products).hasSize(2)
                .extracting("productNumber", "name", "sellingStatus")
                .containsExactlyInAnyOrder(
                        tuple( "001", "아메리카노", SELLING),
                        tuple("002", "카페라떼", HOLD)
                );

 

위에 given 부분에서 001 상품은 SEELING, 002 상품이 HOLD 였으므로, 리스트의 크기는 2여야 한다.

해당 부분은 ⭐️hasSize() 메서드로 검증하고 있다.

 

List에 대한 검증은 ⭐️extracting() 메서드를 이용하면 좋다.

extracting() 메서드로 리스트 안에 들어있는 Product 인스턴스들에서 productNumber, name, sellingStatus의 값만 추출하고, ⭐️containsExactlyInAnyOrder() 메서드와 tuple을 통해 리스트에 특정 값들이 들어있는지 검증하고 있다.