스프링 @JdbcTest

후니팍·2023년 4월 30일
1
post-thumbnail

글 작성 계기

테스트 코드를 작성할 때 @SpringBootTest, @JdbcTest, @WebMvcTest 차이를 확실하게 알지 않고 코드를 작성한 것 같았습니다. 차이를 제대로 알아보고자 이번 글을 올리게 되었습니다.


@JdbcTest

JDBC 관련 테스트입니다. Respository(Dao) 클래스를 위한 테스트입니다. 데이터가 정상적으로 저장되고 정상적으로 호출되는지 확인하는 것을 목적으로 한 테스트 어노테이션입니다.

특징

  • jdbc와 관련 있는 빈들만 auto configuration하는 테스트
  • default로 in-memory 데이터베이스를 사용
  • 각각의 테스트는 하나의 트랜젝션이고, 테스트가 끝나면 원래 상태의 데이터베이스로 롤백됨

특징을 보면 굳이 스프링 빈을 모두 주입하지 않고, 상대적으로 가볍게 테스트 할 수 있다는 장점이 있습니다. 또, 데이터베이스도 직접 롤백하지 않아도 되죠.

예시 코드입니다.

@JdbcTest
class ProductDaoTest {

    @Autowired
    private DataSource dataSource;

    private JdbcTemplate jdbcTemplate;

    private ProductDao productDao;

    @BeforeEach
    void setUp() {
        productDao = new ProductDao(dataSource);
        jdbcTemplate = new JdbcTemplate(dataSource);
    }
    
    ...
}

DataSource를 주입시키고 JdbcTemplate와 Dao 인스턴스를 만들어주었습니다.


@Sql

테스트를 실행하기 전 원하는 sql문을 실행시켜주는 어노테이션입니다. 단위 테스트를 위해 값이 미리 세팅되어야하는 상황이 발생하는데요. 저는 patch, get, delete 메소드에서 이런 상황이 발생했습니다. 처음에는 테스트 메서드 안에서 JdbcTemplate로 직접 값을 넣어주었습니다.
아래는 그 코드입니다.

@Test
@DisplayName("상품 전체 조회 성공")
void findALl_success() {
    // given
    final String sql = "insert into product (name, image, price) values (?, ?, ?)";
    jdbcTemplate.query(sql, "디투", "ditoo.jpg", 1000);
    
    // when
    final List<ProductEntity> allProducts = productDao.findAll();

    // then
    assertAll(
            () -> assertThat(allProducts).hasSize(1),
            () -> assertThat(allProducts.get(0).getName()).isEqualTo("디투"),
            () -> assertThat(allProducts.get(0).getImage()).isEqualTo("ditoo.jpg"),
            () -> assertThat(allProducts.get(0).getPrice()).isEqualTo(1000)
    );
}

@Sql 어노테이션을 이용하여 sql 파일을 불러와 미리 세팅한 상태로 테스트 코드를 작성한다면 given 부분을 조금 더 쉽게 할 수 도 있을 것 같습니다!
아래는 적용한 코드입니다.


@Test
@DisplayName("상품 전체 조회 성공")
@Sql(scripts = "/dummy_data.sql")
void findALl_success() {
    // given, when
    final List<ProductEntity> allProducts = productDao.findAll();

    // then
    assertAll(
            () -> assertThat(allProducts).hasSize(3),
            () -> assertThat(allProducts.get(0).getName()).isEqualTo("pooh"),
            () -> assertThat(allProducts.get(0).getImage()).isEqualTo("pooh.jpg"),
            () -> assertThat(allProducts.get(0).getPrice()).isEqualTo(1_000_000),
            () -> assertThat(allProducts.get(2).getPrice()).isEqualTo(10)
    );
}

sql 파일의 내용은 아래와 같습니다.

truncate table product;
alter table product auto_increment = 1;
insert into product (name, image, price)
values ('pooh', 'pooh.jpg', 1000000),
       ('ditoo', 'ditoo.jpg', 1000000),
       ('pobi', 'pobi.jpg', 10);

테스트 실행 전에 sql로 값을 세팅하고 시작하면 더 깔끔한 테스트 코드를 작성할 수 있습니다.


문제 상황 발생

sql문을 보시면 alter table product auto_increment = 1;이 있습니다. 이는 pk가 auto increment인 경우에 pk를 다시 1부터 시작한다는 쿼리인데요. 1부터 시작하게 해주지 않으면 테스트가 끝나고 DB가 롤백이 되더라도 pk가 1부터 시작이 되는 것이 아니라 마지막 pk의 다음 숫자부터 시작하게 됩니다.

예를 들어보겠습니다. @Sql을 이용하여 윗 sql 스크립트 파일을 세팅하고 테스트를 2개 돌린다고 가정했을 때, 첫 테스트에서는 pk가 1, 2, 3로 1부터 시작하게 되지만 두 번째 테스트에서는 pk가 4, 5, 6으로 설정됩니다. 데이터는 롤백이 되지만 pk 시작 번호는 롤백이 되지 않은 것이죠.

그리고 이 문법은 mysql 문법이기 때문에 h2 데이터베이스를 사용한다면 application.properties에서 spring.datasource.url=jdbc:h2:mem:productDb;MODE=MySQL를 통해 mysql 모드로 실행되도록 해야합니다.


두 번째 문제 상황 발생

mysql모드로 설정하고 쿼리를 실행시켰는데도 문제가 발생했는데요.
org.springframework.jdbc.datasource.init.ScriptStatementFailedException: Failed to execute SQL script statement #2 of class path resource [dummy_data.sql]: alter table product auto_increment = 1; nested exception is org.h2.jdbc.JdbcSQLSyntaxErrorException: Syntax error in SQL statement "alter table product [*]auto_increment = 1"; expected "., ADD, SET, RENAME, DROP, ALTER"; SQL statement:
alter table product auto_increment = 1 [42001-214]
위와 같은 에러가 발생했습니다. 분명 mysql모드로 바꾸었는데 왜 h2 데이터베이스 sql 오류로 나타났을까요...?

@JdbcTest를 쓰면서 mysql 설정으로 변경해주지 않아서 그렇습니다.
@JdbcTest 특징 부분에서 언급했던

default로 in-memory 데이터베이스를 사용

때문에 테스트가 자동으로 in-memory h2 데이터베이스를 사용하게 된 것이죠. 이 설정을 바꿔주기 위해 @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)를 사용했습니다.

여기서 Replace를 살펴보자면 아래 사진과 같이 3가지 옵션이 있습니다. default는 ANY입니다.

  • ANY: 자동 설정이나 수동 정의에 상관 없이 DataSource 빈을 교체
  • AUTO_CONFIGURED: 자동 설정된 경우에 DataSource 빈을 교체
  • NONE: default DataSource를 교체하지 않음

application.properties에 설정을 해두어도 Replace.ANY 설정으로 in-memory로 바뀌어버린 설정을 None으로 바꾸어 mysql를 사용하도록 설정했습니다.

아래는 바뀐 코드입니다.

@JdbcTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class ProductDaoTest {

    @Autowired
    private DataSource dataSource;

    private JdbcTemplate jdbcTemplate;

    private ProductDao productDao;

    @BeforeEach
    void setUp() {
        productDao = new ProductDao(dataSource);
        jdbcTemplate = new JdbcTemplate(dataSource);
    }

    @Test
    @DisplayName("상품 전체 조회 성공")
    @Sql(scripts = "/dummy_data.sql")
    void findALl_success() {
        // given, when
        final List<ProductEntity> allProducts = productDao.findAll();

        // then
        assertAll(
                () -> assertThat(allProducts).hasSize(3),
                () -> assertThat(allProducts.get(0).getName()).isEqualTo("pooh"),
                () -> assertThat(allProducts.get(0).getImage()).isEqualTo("pooh.jpg"),
                () -> assertThat(allProducts.get(0).getPrice()).isEqualTo(1_000_000),
                () -> assertThat(allProducts.get(2).getPrice()).isEqualTo(10)
        );
    }
    
    ...
}

마무리

에러를 해결하면서 왜 안되지 하며 저와 비슷한 상황을 겪은 사람들을 검색했습니다. 사실 에러 메시지에서 h2 설정으로 되어있다는 것을 알 수 있었는데, 왜 변하지 않았을까 고민하기 보다는 일단 구글링을 했던 것 같습니다. 조금 더 근본적인 해결책을 찾는 습관을 길러야 할 것 같습니다.

profile
영차영차

1개의 댓글

comment-user-thumbnail
2023년 4월 30일

마무리 부분이 인상적이네요

답글 달기