실제 서버 실행 환경에서 사용되는 DB를 그대로 테스트에서 이용하는 것은 문제가 존재한다. 데이터 접근 기술을 테스트하기 위해 단위테스트에서 3개의 레코드를 create하고 추가된 레코드가 3개인지 검증하는 테스트 코드에서 만약 기존 DB(DB에는 레코드가 많은 상태, N개로 가정)를 이용한다면 create후에 N+3개가 될 것이다.
실제 서버가 구동될 때 사용되는 DB와 testDB는 분리하는 것이 여러모로 좋다.
spring.profiles.active=test
spring.datasource.url=jdbc:h2:tcp://localhost/~/testcase
spring.datasource.username=sa
spring.datasource.password=
test 리포지토리의 application.properties에도 testcase.db를 생성하여 이를 설정에 적용해준다.(테스트를 위해 테이블도 생성해주자.) @SpringBootTest가 붙은 테스트 클래스(ItemRepositoryTest)는 main실행 클래스 파일(@SpringBootApplication가 붙은 클래스)을 읽게 된다. 그에 따라 기존의 Config등을 적용한다.
BeforeEach, AfterEach를 사용해서 단위 테스트마다 이전과 이후에 해야할 작업을 정의할 수 있다. Before영역에서 트랜잭션을 걸어주고 After영역에서 롤백을 해준다면 독립적인 단위테스트를 구성할 수 있다.
@Autowired
PlatformTransactionManager transactionManager;
TransactionStatus status;
@BeforeEach
void beforeEach() {
// 트랜잭션 시작
status = transactionManager.getTransaction(new DefaultTransactionDefinition());
}
@AfterEach
void afterEach() {
//MemoryItemRepository 의 경우 제한적으로 사용
if (itemRepository instanceof MemoryItemRepository) {
((MemoryItemRepository) itemRepository).clearStore();
}
// 트랜잭션 롤백
transactionManager.rollback(status);
}
Autowired로 스프링에 자동등록된 transactionManager를 주입받자. 까먹지 않았다면 스프링은 설치된 라이브러리를 고려하여 적절한 PlatformTransactionManager의 구현체를 주입해준다는 것을 인지하고 있을 것이다.
주입받은 transactionManager로 하여금 트랜잭션 로직을 완성해준다. 이를 통해 트랜잭션을 활용하여 독립적인 테스트를 수행할 수 있게 되었지만 더 나은 방법이 존재한다.
@Transactional는 서비스 계층에서 사용했었다. 트랜잭션 애노테이션을 소지한 서비스 메서드가 리포지토리의 기능을 호출한다. 그렇게 되면 동기화 매니저에 트랜잭션이 걸린(정확히 말하면 오토커밋이 false가 된) 커넥션 객체가 등록되어 리포지토리의 jdbcTemplate의 DB와 소통하는 메서드는 연결을 위해 커넥션 객체를 동기화 매니저의 로컬 쓰레드에서 가져온다.
리포지토리의 메서드가 정상적으로 처리되었다면 커밋이되고 도중에 예외가 발생한다면 트랜잭션은 롤백된다.
하지만 테스트 환경에서의 트랜잭션은 정상적으로 처리되었어도 커밋이 아닌 롤백이 된다.
스프링이 관리하는 테스트 컨텍스트는 실행이전 Transactional 적용 여부를 확인한다. 만약 적용대상이라면 트랜잭션을 걸고 모든 로직이 성공할 경우 롤백을 진행한다. 만약 테스트에 실패할 경우(리포지토리 기능이 예상과 다르게 예외를 발생시킨다면) 이 역시 롤백을 진행하면서, 예외를 발생시켜 테스트가 실패하게 된다.
서비스나 리포지토리에 존재하는 @Transactional
테스트 코드에서도 역시 서비스나 리포지토리의 기능을 사용한다. 실제 기능에서는 이 레벨에서 Transacitonal 애노테이션이 적용되는데 테스트 클래스에서도 애노테이션이 붙고 서비스 or 리포지토리 레벨에서도 붙는다면 테스트 실행이 종료될 때까지 모든 코드가 같은 트랜잭션 범위에 들어간다(트랜잭션 전파 -> 후에 자세히 다루겠다)
롤백을 해버릴 경우 DB에 보이는 데이터는 없을 것이다. 실제로 DB에 잘 변화가 적용되는지 보기 위해 @Commit을 단위 테스트 메서드에 주어 DB에 적용된 모습을 볼 수도 있다.(다만 보고나서는 지워야 된다.)
테스트를 위해 데이터베이스를 설치하고 운영하는 것은 어느정도 복잡한 과정을 요구한다. 결국 테스트마다 데이터가 잠시 생성되었다가 롤백된다면 모든 단위 테스트에 있어 끝은 빈 데이터베이스이다. 즉 큰 데이터베이스 용량이 필요하지 않으므로 스프링은 임베디드 모드 DB를 한정적으로 지원한다.(H2와 같은 자바로 개발된 DB의 경우 JVM안에서 메모리 모드로 동작하게끔 만들 수 있다.)
임베디드 모드를 사용하는 법은 매우 간단하다. 테스트 application.properties에 구성해놓은 테스트DB 정보들을 모두 지워 디폴트 상태로 만들면 된다.(H2와 같은 임베디드 모드 DB를 지원하는 경우만 해당사항이다.)
메모리에 생성된 DB는 테이블 생성이 필요하다. 실제 DB툴로 이를 접근할 수 없기에 임베디드 모드 DB를 사용할 때는 DB 초기화 작업이 필요하다.
test의 resources 폴더에 schema.sql파일을 만들어 테이블 생성 sql문을 작성해서 넣어놓을 경우 spring.datasource.schema에 쿼리문이 적용된다. 이를 통해 임베디드 모드 DB를 활용하기 전에 초기화로써 테이블을 구성할 수 있다.
schema.sql외에 실제 CRUD와 같은 Table수준에서 적용하는 쿼리문도 적용하는 초기화를 진행할 수 있다. data.sql을 schema.sql처럼 resources위치에 생성하고 쿼리문을 작성한다.
적용하는 DB 형태에 따라 spring.datasource.initialization-mode=always, embedded 등을 설정한 후
spring.datasource.data=classpath:data.sql
spring.datasource.schema=classpath:schema.sql
과 같이 적용할 수 있다. classpath의 위치는 main과 test에서의 resources이다. (path는 변경가능하며, data.sql,schema.sql외에 다른 커스텀 .sql파일을 만들어 등록해서 사용할 수도 있다.)
schema.sql 외부 DB 적용
이러한 초기화는 외부DB를 사용하더라도 적용이 가능하지만 설정파일에 대한 수정이 필요하다.
spring.datasource.initialization-mode=always라고 설정해야하며, 디폴트는embedded로 임베디드 DB 모드만 이용할 때 적용된다.