데이터 접근 기술에 대해서 더 알아보기 전에 데이터베이스에 연동하는 테스트에 대해서 알아보자.
test - application.properties 수정
src/test/resources/application.properties
spring.profiles.active=test
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
logging.level.org.springframework.jdbc=debug
ItemServiceApplication
@Import(JdbcTemplateV3Config.class)
@SpringBootApplication(scanBasePackages = "hello.itemservice.web")
public class ItemServiceApplication {
ItemRepositoryTest
는 @SpringBootTest
를 사용한다. @SpringBootTest
는 SpringBootApplication
를 찾아서 설정으로 사용한다.@SpringBootApplication
설정이 과거에는 MemoryConfig.class
를 사용하다가 이제는 JdbcTemplateV3Config.class
를 사용하도록 변경되었다. 따라서 테스트도 JdbcTemplate
을 통해 실제 데이터베이스를 호출하게 된다.테스트 실행
ItemRepositoryTest 실행
updateitem()
: 성공save()
: 성공findItems()
: 실패 @Test
void findItems() {
//given
Item item1 = new Item("itemA-1", 10000, 10);
Item item2 = new Item("itemA-2", 20000, 20);
Item item3 = new Item("itemB-1", 30000, 30);
itemRepository.save(item1);
itemRepository.save(item2);
itemRepository.save(item3);
//둘 다 없음 검증
test(null, null, item1, item2, item3);
test("", null, item1, item2, item3);
//itemName 검증
test("itemA", null, item1, item2);
test("temA", null, item1, item2);
test("itemB", null, item3);
//maxPrice 검증
test(null, 10000, item1);
//둘 다 있음 검증
test("itemA", 10000, item1);
}
실패 원인
문제는 H2 데이터베이스에 이미 과거에 서버를 실행하면서 저장했던 데이터가 보관되어 있기 때문이다.
H2 데이터베이스를 용도에 따라 2가지로 구분하면 된다.
jdbc:h2:tcp://localhost/~/test
: local에서 접근하는 서버 전용 데이터베이스jdbc:h2:tcp://localhost/~/testcase
: test 케이스에서 사용하는 전용 데이터베이스데이터베이스 파일을 생성 후 테이블을 생성했다.
create table item
(
id bigint generated by default as identity,
item_name varchar(10),
price integer,
quantity integer,
primary key (id)
);
test - application.properties 변경
spring.datasource.url=jdbc:h2:tcp://localhost/~/testcase
이제 테스트를 실행
findItems()
테스트를 다시 실행하면 테스트에 실패한다.save()
같은 다른 테스트가 먼저 실행되고 나서 findItems()
를 실행할 때도 나타난다. 다른 테스트에서 이미 데이터를 추가했기 때문이다. 결과적으로 테스트 데이터가 오염된 것이다.테스트에서 매우 중요한 원칙
트랜잭션과 롤백 전략
@BeforeEach
, @AfterEach
라는 편리한 기능을ItemRepositoryTest 소스 추가
@Autowired
ItemRepository itemRepository;
//트랜잭션 관련 코드
@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);
}
PlatformTransactionManager
를 주입 받아서 사용하면 된다. 참고로 스프링 부트는 자동으로 적절한 트랜잭션 매니저를 스프링 빈으로 등록해준다.transactionManager.getTransaction(new DefaultTransactionDefinition())
로 트랜잭션을 시작한다.@AfterEach
: 각각의 테스트 케이스가 완료된 직후에 호출된다. 따라서 여기서 트랜잭션을 롤백하면 된다. 그러면 데이터를 트랜잭션 실행 전 상태로 복구할 수 있다.transactionManager.rollback(status)
로 트랜잭션을 롤백한다.TestCase 데이터베이스에 데이터를 모두 삭제하고 다시 테스트를 돌려보면 성공하는 것을 확인 할 수 있다.
스프링은 테스트 데이터 초기화를 위해 트랜잭션을 적용하고 롤백하는 방식을 @Transactional
애노테이션 하나로 깔끔하게 해결해준다.
ItemRepositoryTest @Transactional 추가
@Transactional
@SpringBootTest
class ItemRepositoryTest {
@Autowired
ItemRepository itemRepository;
//트랜잭션 관련 코드
// @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);
}
반복 실행해도 계속 성공한다.
@Transactional
애노테이션은 로직이 성공적으로 수행되면 커밋하도록 동작한다.@Transactional
애노테이션을 테스트에서 사용하면 아주 특별하게 동작한다.@Transactional
이 테스트에 있으면 스프링은 테스트를 트랜잭션 안에서 실행하고, 테스트가 끝나면 트랜잭션을 자동으로 롤백시켜 버린다.@Transactional이 적용된 테스트 동작 방식
@Transactional
이 있으면 테스트 로직 실행 시 테스트가 끝날 때 까지 모든 로직은 트랜잭션 안에서 수행되고 테스트가 끝나면 트랜잭션을 강제로 롤백한다. 롤백에 의해 앞서 데이터베이스에 저장된 데이터는 제거된다.강제로 커밋하기 - @Commit
가끔은 데이터베이스에 데이터가 잘 보관 되었는지 최종 결과를 눈으로 확인하고 싶을 때가 있다. 이럴 때는 @Commit
을 클래스 또는 메서드에 붙이면 테스트 종료 후 롤백 대신 커밋이 호출된다. @Rollback(value = false)
를 사용해도 동일한 결과가 나온다.
임베디드 모드
ItemServiceApplication 변경
package hello.itemservice;
import hello.itemservice.config.JdbcTemplateV3Config;
import hello.itemservice.repository.ItemRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Profile;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import javax.sql.DataSource;
@Slf4j
//@Import(MemoryConfig.class)
//@Import(JdbcTemplateV1Config.class)
//@Import(JdbcTemplateV2Config.class)
@Import(JdbcTemplateV3Config.class)
@SpringBootApplication(scanBasePackages = "hello.itemservice.web")
public class ItemServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ItemServiceApplication.class, args);
}
@Bean
@Profile("local")
public TestDataInit testDataInit(ItemRepository itemRepository) {
return new TestDataInit(itemRepository);
}
@Bean
@Profile("test")
public DataSource dataSource() {
log.info("메모리 데이터베이스 초기화");
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;
}
}
@Profile("test")
dataSource()
jdbc:h2:mem:db
: 이 부분이 중요하다. 데이터소스를 만들때 이렇게만 적으면 임베디드 모드(메모리모드)로 동작하는 H2 데이터베이스를 사용할 수 있다.DB_CLOSE_DELAY=-1
: 임베디드 모드에서는 데이터베이스 커넥션 연결이 모두 끊어지면 데이터베이스도 종료되는데, 그것을 방지하는 설정이다.H2 데이터베이스 서버를 끄고 ItemRepositoryTest
Table "ITEM" not found
이 부분이 핵심이다. 데이터베이스 테이블이 없는 것이다.스프링은.. 다양한 기능을 제공한다
스프링 부트 - 기본 SQL 스크립트를 사용해서 데이터베이스를 초기화하는 기능
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)
);
ItemRepositoryTest 테스트
임베디드 모드 DB 관련 설정을 모두 주석처리했다.
설정을 하지 않아도 정상 수행 된다.
참고
김영한: 스프링 DB 2편 - 데이터 접근 활용 기술(인프런)
Github - https://github.com/b2b2004/Spring_DB