개요

통합 테스트 코드에서는 실제 데이터베이스를 연결해서 작업하였다. 하지만 User Id 를 통한 List 조회 시에 테스트를 위해 사용한 데이터뿐만 아니라 실제 데이터베이스에 들어가있던 데이터들도 함께 조회되면서 테스트 검증에 어려움을 겪었다.

이를 해결하기 위해 테스트 데이터베이스를 분리하였지만 데이터베이스가 비어있어 연관관계 매핑에 문제가 발생하였다.

Test Code

@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private String email;
}

@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    private User user;

    private String name;

    private String description;

    private int price;

}

다음은 두 개의 엔티티이다. 데이터베이스에 두 개의 테이블로 구성될 것이며 가장 간단하게 연관관계를 가지도록 만들었다.

@Service
@RequiredArgsConstructor
public class ProductService {
    private final ProductRepository productRepository;

    public Product createProduct(Long userId, String name, String description, int price) {
        return productRepository.save(Product.builder()
                .user(User.builder().id(userId).build())
                .name(name)
                .description(description)
                .price(price)
                .build());
    }
}

아주 간단한 서비스 코드를 작성하고

@SpringBootTest
class ProductServiceTest {
    @Autowired
    private ProductService productService;

    @Autowired
    private ProductRepository productRepository;

    @Test
    void createProduct() {
        // Given
        Long userId = 1L;
        String name = "product";
        String description = "description";
        int price = 1000;

        // When
        Product product = productService.createProduct(userId, name, description, price);

        // Then
        assertThat(product.getName()).isEqualTo(name);
    }
}

가장 간단한 테스트 코드를 작성한다면 해당 테스트는 환경에 따라 실패할수도 있고 성공할 수도 있다.

실패한다면 아마 아래와 같은 메세지가 뜰 가능성이 높다.

image

해당 메세지는 User 에 ID 값이 1L 인 유저가 존재하지 않는데, Product 를 생성하면서 매핑하려한다는 뜻이다.

하지만 데이터베이스에 User 생성해준다면 아래와같이 바로 성공 메세지가 떠오른다.

image

환경에 따라 같은 테스트가 실패하고 성공한다니, 테스트에 적합하지 않다..!

위 같은 상황을 방지하고자 테스트 데이터베이스를 분리해보도록 하자.

Test DataBase 분리

image

본 게시글에서는 MySQL 을 동일하게 Test DB 로 사용하도록 하겠다.

우선 test/resource/application.yml 파일을 작성해주어야한다.

여기서 3가지 설정이 주요하다.

  • spring.jpa.hibernate.ddl-auto: create
    • 애플리케이션 시작 시점에 Hibernate가 데이터베이스 스키마를 생성한다.
    • 대부분이 사용하는 update 와 달리 스키마를 항상 새로 만들어내기에 주의해야한다.
  • spring.jpa.defer-datasource-initialization: true
    • 데이터 소스 초기화를 지연시키는 방법을 지정한다. true로 설정하면, 데이터베이스 연결을 초기화하는 것을 미루고 애플리케이션 구동 중에 필요한 시점에서 연결을 수행한다.
  • spring.sql.init.mode: always
    • SQL 초기화를 항상 수행할 것인지를 지정한다. always로 설정하면, 애플리케이션 시작 시점에 SQL 초기화를 항상 수행합니다. SQL 초기화는 schema.sql 및 data.sql 파일을 실행하여 데이터베이스 스키마와 초기 데이터를 설정한다. 이 설정은 스키마 및 데이터가 변경되는 경우에도 항상 초기화를 수행하기 때문에 데이터베이스를 재설정하고자 할 때 사용할 수 있다. 다만, SpringBoot 2.4x 이하 버전이라면 이 설정을 사용하지 않고 spring.datasource.initialization-mode=always로 설정해줘야 한다.

초기 데이터 삽입

image

image

schema.sql 과 data.sql 이다.

말 그대로 스키마를 생성하고 데이터를 넣을 수 있는 파일이다. 스키마는 JPA 기본 스키마 생성 매커니즘으로 생성되지만 위와 같이 사용자 지정으로 생성할 수도 있다. 이때에는 ddl-auto 를 none 으로 설정하는 것을 잊지 말아야한다.

실제 테스트코드도 작성해보았을 때 성공하는 것과 DB 에 데이터가 남아있는 것을 눈으로 확인할 수 있다!

image

image

하지만 이 방법도 문제가 있다. 모든 테스트에 data 가 들어가기 때문에 UserService 테스트에서는 2명의 유저가 이미 존재하기에 어떤 식으로든 문제가 발생할 여지가 존재한다.

이를 해결하기 위해 데이터를 사용자가 선택해 삽입할 수 있도록 하자!

@Sql

이를 위해 먼저 삽입할 데이터를 나눠주어야한다. 도메인 별로 나눌 수도 있고, 각 테스트 별로 필요한 데이터끼리 묶어줄 수도 있겠다.

여기서는 도메인 별로 나누도록 하겠다.

기존 data.sql 의 내용을 지우고

image

image

두 개의 sql 파일을 생성하였다.

    @Test
    @Sql("/user.sql")
    void createProduct() { ... }

그리고 테스트 메서드에 @Sql 어노테이션을 사용해 필요한 데이터를 삽입한다. 이렇게 되면 해당 메서드가 실행될 때는 user.sql 이 실행되 필요한 데이터가 들어가기에 테스트가 성공한다.

UserService 에서는 필요하지 않은 데이터를 삽입하지 않아도 되기에 테스트 간 데이터를 분리할 수 있다!

    @Test
    @Sql(scripts = {"/user.sql", "/product.sql"})
    void createProduct() { ... }

만약 user 이외에 product 데이터도 삽입이 필요하다면 위와 같이 여러 데이터를 삽입할 수 있다.

@Sql 애노테이션은 별도 설정을 하지 않으면 @ContextConfiguration에 지정한 설정 정보에 있는 DataSource 빈을 사용해서 스크립트를 실행하고, 트랜잭션 관리자가 존재할 경우 해당 트랜잭션 관리자를 이용해서 트랜잭션 범위 내에서 스크립트를 실행한다.

@SpringBootTest
class ProductServiceTest {
    @Autowired
    private ProductService productService;

    @Test
    @Sql(scripts = {"/user.sql", "/product.sql"})
    void createProduct() {
        // Given
        Long userId = 1L;
        String name = "product";
        String description = "description";
        int price = 1000;

        // When
        Product product = productService.createProduct(userId, name, description, price);

        // Then
        assertThat(product.getName()).isEqualTo(name);
    }

    @Test
    void createProduct2() {
        // Given
        Long userId = 1L;
        String name = "product";
        String description = "description";
        int price = 1000;

        // When
        Product product = productService.createProduct(userId, name, description, price);

        // Then
        assertThat(product.getName()).isEqualTo(name);
    }
}

따라서 위와 같이 작성하더라도 createProdut2 에서는 User 데이터가 존재하지 않기에 에러가 발생한다.

image

또한 @Sql 어노테이션은 클래스에서 선언되어 내부 메서드 전체에 적용시킬 수도 있으며 @SqlConfig 를 통해 SQL 스크립트에 대한 로컬 구성도 가능하다!

테스트 코드에서 데이터베이스를 분리하고 각 테스트마다 필요한 데이터를 삽입해 테스트를 진행하도록 하자!

코드

Github Code

참고

SpringBoot 테스트시 초기 SQL 데이터 삽입 - Test SQL

Spring - Spring Boot 초기 데이터 설정 (data.sql)

업데이트:

댓글남기기