애플리케이션을 작성할 때 테스트가 매우 중요하다는 것은 다들 알 것이다.
내가 작성한 코드를 테스트하는 것은 내 코드에만 접근하면 된다. 하지만 데이터베이스에 접근하는 코드를 테스트하기 위해서는 어떻게 해야할까?
Spring에서는 보통 Repository
계층에서 DB에 접근한다.(개발자마다 상이할 수 있으나 이 글에서는 Repository에서 접근한다고 가정) Repository
를 테스트할 때에는 당연히 DB에 접근이 필요하다. 어떤 방법으로 Repository를 테스트할 수 있는 지 알아보자.
프로젝트의 main 패키지에서 작동하는 실제 애플리케이션과 테스트는 분리할 필요가 있다.
분리하지 않는다면 실제 웹 애플리케이션에서 등록한 데이터 등이 테스트할 때에도 그대로 남아있기때문에 테스트 그 자체의 독립적인 수행을 하기가 어렵게 된다. 그렇다고 테스트할 때마다 DB를 초기화한다면 실제 DB의 데이터도 삭제되기 때문에 보통 테스트용 DB를 따로 분리한다.
가장 간단한 방법은 test
패키지 내에 있는 application.properties(혹은 application.yml)
파일에 test용 datasource를 작성하는 것이다.
src/main/resources/application.properties
spring.profiles.active=local
spring.datasource.url=jdbc:h2:tcp://localhost/~/local
spring.datasource.username=sa
src/test/resources/application.properties
spring.profiles.active=test
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
위와 같이 main 패키지와 test 패키지에 각각 설정파일을 만들어주었다. spring.datasource.url
속성을 보면 main DB이름이 다르다. (local과 test)
이제 실제 DB와 테스트용 DB가 잘 분리되었다.
💡 spring.profiles.active 속성을 통해 해당 설정 파일의 프로필 이름을 지정할 수 있다.
만약 코드에서 @Profile("local")로 어떤 메서드를 설정해주었다면 test 시에는 해당 메서드가 실행되지 않는다.
좋은 테스트는 여러가지 원칙이 있다.
단위 테스트의 원칙으로 유명한 F.I.R.S.T 원칙은 다음과 같다.
테스트를 수행할 때마다 DB 트랜잭션을 롤백해준다면 중요한 테스트 원칙인 Isolated와 Repeatable을 지킬 수 있을 것이다. 이 두가지 원칙지 지켜지지 않는 경우는 다른 테스트의 결과가 DB에 커밋되어 남아있거나, 같은 테스트를 여러번 하는 경우에도 DB에 데이터가 남게되어 다음 테스트에 영향을 주는 경우이다. 이는 트랜잭션을 롤백함으로써 쉽게 해결할 수 있다.
1. 트랜잭션 시작
2. 테스트 A 실행
3. 트랜잭션 롤백
4. 트랜잭션 시작
5. 테스트 B 실행
6. 트랜잭션 롤백
가장 직관적인 방법은 테스트의 @BeforeEach
, @AfterEach
를 사용하여 직접 트랜잭션을 추가하는 것이다.
@Autowired
PlaformTransactionManaber transactionManager;
TransactionStatus status;
@BeforeEach
void beforeEach() {
//트랜잭션 시작
status = transactionManager.getTransaction(new DefaultTransactionDefinition());
}
@AfterEach
void afterEach() {
//트랜잭션 롤백
transactionManager.rollback(status);
}
트랜잭션매니저를 주입받아 각각의 테스트를 수행하기 전에(@BeforeEach
) 트랜잭션을 시작하는 코드를 실행한다. @BeforeEach
는 각각의 테스트를 수행하기 전에 실행되는 메서드이다. beforeEach 메서드에서 트랜잭션을 가져온다. @AfterEach
는 각각의 테스트를 수행한 후에 실행되는 메서드이다. 테스트는 독립적이고 반복할 수 있어야 하므로 가져온 트랜잭션을 롤백한다.
transactionManager.getTransaction(new DefaultTransactionDefinition()); // 트랜잭션 속성이 기본값인 트랜잭션
@Transactional 애노테이션은 트랜잭션을 프록시로 하여 해당 메서드에서 트랜잭션을 수행해주고 성공 시 커밋, 실패 시 자동으로 롤백해주는 매우 편리한 애노테이션이다. 테스트에서도 사용할 수 있다.
@Transactional // 트랜잭션 추가
@SpringBootTest
class ItemRepositoryTest {
@Autowired
ItemRepository itemRepository;
}
위 코드를 보면 Transactional 애노테이션만 붙여줌으로 트랜잭션 관련 코드를 모두 삭제했다.
삭제된 코드들
PlaformTransactionManaber transactionManager;
TransactionStatus status;
@BeforeEach
...
@AfterEach
...
@Transactional
을 테스트의 클래스 범위에서 사용하면 각각의 테스트 메서드에 모두 적용된다. 만약 하나의 메서드만 트랜잭션 처리를 하고싶다면 메서드에 붙이면된다.
@Transactional
void 테스트(){
...
}
(참조: 김영한의 DB접근기술)
💡
@Transactional
은 테스트에서 사용되는 경우 테스트의 성공 실패 여부를 떠나서 무조건 롤백해준다. 당연히 테스트가 아닌 경우 성공 시 커밋해준다. 하지만 롤백해야하는 상황에서도 강제로 커밋하고 싶을 수도 있다. 이 때는@Commit
애노테이션을 사용할 수 있다.
테스트는 말 그대로 테스트이기 때문에 계속 롤백되는 DB를 굳이 설치해서 설정하는 것은 귀찮을 수도 있다.
💡 H2 데이터베이스는 자바로 개발되어 있고, JVM안에서 메모리 모드로 동작하는 특별한 기능을 제공한다.
즉, 애플리케이션을 실행할 때 H2 데이터베이스도 해당 JVM 메모리에 포함해서 함께 실행할 수 있다. DB를 애플리케이션에 내장해서 함께 실행한다고 해서 임베디드 모드라 한다.
test Profile일 때만 Bean으로 데이터소스를 등록한다.
@Import(SpringConfiguration.class)
@SpringBootApplication(scanBasePackages = "hello.example")
public class ItemServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ItemServiceApplication.class, args);
}
@Bean
@Profile("test")
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.h2.Driver");
dataSource.setUrl("jdbc:h2:mem:db;DB_CLOSE_DELAY=-1");
dataSource.setUsername("sa");
dataSource.setPassword("");
return dataSource;
}
}
dataSource의 url인 jdbc:h2:mem:db;DB_CLOSE_DELAY=-1
는 내장 메모리를 사용한 DB를 데이터소스로 등록한다는 뜻이다.
DB를 만들었으니 테이블을 생성해야한다.
테이블 생성
src/test/resources/schema.sql
drop table if exists item CASCADE;
create table item
(
id bigint generated by default as identity,
item_name varchar(10),
price integer,
quantity integer,
primary key (id)
);
스프링이 임베디드 DB를 사용할 때 sql문을 미리 실행하도록 도와준다. 주의할 점은 꼭 src/test/resources/schema.sql
위치와 sql 파일 이름(schema.sql)을 지켜야한다.
스프링부트에서는 Bean으로 등록하는 등 번거로운 작업을 할 필요 없다. 단지 데이터베이스에 대한 내용을 아무것도 적지 않으면 테스트는 임베디드 DB로 작동한다.