스프링 DB 테스트 격리하기

홍혁준·2023년 5월 7일
4

개요

이번 레벨 2 장바구니 미션을 진행하면서, 본격적으로 통합테스트를 작성해봤습니다.

DB가 연관되서 통합테스트를 진행하려다보니 테스트간 디비 격리가 잘 되지 않았는데요.

이번 포스팅에서는 인수테스트, 단위테스트, 프로덕션간의 디비를 분리하고, 각각의 테스트 메서드를 어떻게 격리했는지를 보여드리도록 하겠습니다.

해당 포스팅에서 언급하는 단위테스트는 persistence layer에서 사용하는 JdbcTest이고,
인수테스트는 RestAssured 기준으로 작성하였습니다.

분리해야할 환경

프로덕션DB와 테스트DB

가장 먼저 분리해야할 환경입니다.

별도의 설정을 하지 않으면, 테스트에서는 DB와 관련된 설정을 할 때 Production의 application.properties를 참고하여 DataSource를 만들고, 해당 DataSource로 DB연결을 하여 테스트를 진행합니다.

프로덕션 코드가 실행되는 도중 인수 테스트를 실행하면 테스트가 프로덕션 DB에 영향을 주게 됩니다.

이러한 불상사를 방지하기 위해, 테스트 전용 application.propreties를 정의하여 datasource의 url을 분리합니다.

위 처럼 test에서 application.properties를 정의하고 datasource.url을 다르게 정의하면 프로덕션과 테스트간의 DB를 분리할 수 있습니다.

인수 테스트와 단위 테스트

그러면 같은 datasource를 사용하는 인수테스트와 단위테스트는 어떻게 분리해야할까요?

사실 이는 큰 문제가 없습니다. @JdbcTest 어노테이션에 보면 다음과 같은 어노테이션이 붙어있습니다.

@AutoConfigureTestDataBase라는 어노테이션은 가짜 DB를 만들어서, DB관련한 테스트를 쉽게 할 수 있도록 도움을 줍니다.

실제 test가 실행될 떄 로그를 보면 완전 다른 datasource.url인 ‘jdbc:h2:mem:6d095…’가 사용된 것을 볼 수가 있습니다.

그렇기에, jdbcTest 어노테이션을 붙이면 다른 테스트에 영향이 가지 않도록 DB환경을 분리를 할 수 있습니다.

테스트 방식

테스트 메서드 마다 DB에 값 넣기

가장 처음에 활용한 방법입니다. DB관련 로직을 수행할 때 테스트 데이터를 매 실행시마다 넣어주도록 하였습니다.

Repository의 save 메서드 활용하기

가장 처음에 활용한 방법입니다. 조회,수정,삭제를 테스트하기위해 repository의 save 메서드를 활용하여, 초기 데이터를 넣었습니다.

하지만 이 방법은 모든 CRUD 로직 테스트가 save 메서드에 의존적이게 되기도 하고, 값이 여러개 있는 케이스를 테스트하기 위해 여러번 쿼리를 날리게 됩니다.

batchInsert를 정의하면 되긴 하지만, 프로덕션에서 사용하지 않는 로직을 테스트를 위해 작성하는 것이 부담스러웠습니다.(심지어 DAO 로직)

Sql 어노테이션으로 값 넣기

그 다음에 찾아 학습한 방식은 Sql 어노테이션을 이용한 방법이었습니다.(역시 밸덩)

Spring JdbcTemplate Unit Testing | Baeldung

testFixture.sql을 정의하고, 테스트 메서드에 붙였습니다.

테스트 로직이 실행되기전에 testFixture.sql이 실행되도록 하였습니다.

이러면 이전 방법보다 여러개의 데이터를 세팅할 떄 connection 비용을 절약할 수도 있고, save 메서드에 의존하지 않게 만들 수 있습니다.

단, 이 방법도 결국엔 매 테스트 메서드마다 connection 비용이 발생합니다.

이정도에서 멈춰도 되지만, 더 좋은 방법이 없을까? 싶어서 다른 방법을 찾아보았습니다.

초기 더미데이터 세팅하고, Transactional 이용하기

Spring에는 @Transactional 이라는 간단한 어노테이션을 이용해서, 디비의 값을 롤백시킬 수 있습니다.

테스트메서드에 붙이면, 테스트가 종료된 후 DB의 값을 롤백해주죠.(스프링 매직!)

물론 테스트에서, @Trasnactional을 지양하는 사람들도 있긴 합니다.

하지만 이는 규모가 크고 여러 명이 협업하는 상황에서 문제가 되는 것이기에, 이정도 규모의 프로젝트에서는 충분히 사용할 수 있다고 생각하여 이를 활용해보았습니다.

BeforeAll에서 데이터 삽입하고, 테스트 진행하기

테스트에서 BeforeAll은 모든 테스트 메서드가 실행되기전에 딱 한번 호출되는 메서드입니다.

그래서 맨 처음에 BeforeAll메서드를 정의하고, 해당 메서드에 @Sql 어노테이션을 붙이면 되지 않을까? 라고 생각하였습니다. 바로 시도해봤죠.

결과는 대실패… 어쩐일인지 테스트 메서드에서 확인을 해봤을 때, 아무런 값이 들어가지 않은 상태로 테스트가 진행되었습니다.

테스트 디비가 생성되기전에 script를 날려서 그런가?라는 생각도 해봤지만,
그랬으면 SqlSytnaxError가 떠야하는데, 정상적으로 테스트가 진행됬으니 디비가 생성된 후 쿼리가 사용되었다는 것을 알 수 있습니다.

혹시 가짜DB가 생성되기 전에 script가 실행되서 그런가 싶었지만, 그런것도 아니었습니다.

Transaction 매니저가 롤백했나? 싶었지만, 로그에서 TransactionManager를 보았을 때 setUp을 rollback한다는 로그는 어디에도 없었습니다.

이 부분은 아직도 파악하지 못한 상태긴 합니다. 혹시 아시는 분은 댓글 부탁드립니다.

data.sql 이용하기

beforeAll에서 초기화 하는것이 막혔으니, 이제는 db가 초기화될 때 딱 한번 실행하는 script인 data.sql을 이용해보기로 하였습니다.

그렇게 test 전용 data.sql을 만들었는데,,,

테스트를 돌려보니 프로덕션의 data.sql과 테스트의 data.sql이 둘 다 실행되는 것이었습니다.

다행히 프로덕션에서는 프로덕션의 data.sql만 실행되었습니다.

그렇다하더라도 의도치 않게 테스트에서 초기 데이터를 설정하는 것에 프로덕션의 의존성이 생겨버렸습니다. 유지보수하기 더 힘들어지곘죠.

프로덕션과 테스트 data.sql 분리하기

test패키지 하위에 있는 data.sql을 적용하는 방법을 찾다가 다음과 같은 옵션을 application.properties에서 발견했습니다.

data.sql의 경로를 임의로 지정할 수 있는 옵션이었죠. test의 application.properties에서 해당 옵션을 적용하여, 테스트 패키지 하위에 있는 data.sql만 적용되도록 하였습니다.

이제, 저희는 data.sql와 application.properties로 JdbcTest에서, 일관된 디비 데이터를 유지할 수 있으므로, JdbcTest 클래스에서 테스트 메서드간 디비 격리를 수행할 수 있습니다.

문제

이 방법에도 한가지 맹점이 있습니다.

@Transactional 어노테이션을 이용하는 방식 Transactional 어노테이션을 사용할 수 없는 RestAssured에서는 위와 같은 방법을 적용할 수 없습니다.

인수 테스트에서 격리

RestAssured에서는 Transactional이 동작하지 않기에, 초기 더미데이터 세팅하고, Transactional 이용하기 와 같은 방법을 사용할 수 없습니다.

이번에 인수테스트에서 테스트간 격리를 위해 시행한 방법은 두 가지였습니다.

하나는 이전에 사용했던 Sql 어노테이션으로 값 넣기 였고, 다른 하나는 @DirtiesContext입니다.

@DirtiesContext

이 어노테이션이 붙어있으면, 현재 테스트가 실행될 때 기존의 ApplicationContext가 존재하더라도, 새로운 ApplicationContext를 로드합니다. 그렇기에, 테이블도 다시 만들어지고, data.sql도 동작하기에 초기상태를 계속 유지할 수 있죠.

단, 이 방법은 컨텍스트를 계속 로드한다는 점에서, 굉장히 좋지 못한 방법입니다.

저는 학습용으로만 한 번 써보고, 그 다음에 바로 지웠습니다.

최종적으로는 @Sql 어노테이션을 이용한는 방법을 택하였습니다.

최종 형태

공통

가장 먼저 테스트용 application.properties를 정의하여서 프로덕션과 테스트 디비를 분리하였습니다. 이는 인수테스트든 단위테스트든 공통적으로 적용되는 사항입니다.

인수 테스트

인수 테스트에서는 fixutre.sql을 정의하여서 매 테스트 메서드가 실행될 떄 fixture.sql이 동작하도록 하였습니다.

fixture에서는 truncate 문과 더미 데이터를 insert하는 script가 있습니다.

단위 테스트

단위 테스트에서는 application.properties의 spring.sql.init.data-location 옵션으로

테스트에서만 사용할 더미데이터가 있는 data.sql을 정의하고, @Transactional 어노테이션을 이용해서 롤백을 하는 방식으로 테스트간 격리를 하였습니다.

profile
끊임없이 의심하고 반증하기

4개의 댓글

comment-user-thumbnail
2023년 5월 7일

역시 킹갓홍실
다양한 방식을 소개해주시고 직접 시도한 내용을 공유해주셔서 정말 좋았습니다
잘읽었습니다

1개의 답글
comment-user-thumbnail
2023년 5월 7일

근데 프로덕션DB와 테스트DB 항목에 있는 첫 번째 사진이 안떠요

1개의 답글