728x90
데이터베이스 연동
데이터 접근 기술에 대해서 더 알아보기 전에 데이터베이스에 연동하는 테스트에 대해서 알아보자. 데이터 접근 기술은 실제 데이터베이스에 접근해서 데이터를 잘 저장하고 조회할 수 있는지 확인하는 것이 필요하다
src/test/resources/application.properties
spring.profiles.active=test
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
#jdbcTemplate sql log
logging.level.org.springframework.jdbc=debug
- Test에 데이터베이스 연결을 한다.
Test 실행
@SpringBootTest
class ItemRepositoryTest {
@Autowired
ItemRepository itemRepository;
@AfterEach
void afterEach() {
//MemoryItemRepository 의 경우 제한적으로 사용
if (itemRepository instanceof MemoryItemRepository) {
((MemoryItemRepository) itemRepository).clearStore();
}
}
@Test
void save() {
//given
Item item = new Item("itemA", 10000, 10);
//when
Item savedItem = itemRepository.save(item);
//then
Item findItem = itemRepository.findById(item.getId()).get();
assertThat(findItem).isEqualTo(savedItem);
}
@Test
void updateItem() {
//given
Item item = new Item("item1", 10000, 10);
Item savedItem = itemRepository.save(item);
Long itemId = savedItem.getId();
//when
ItemUpdateDto updateParam = new ItemUpdateDto("item2", 20000, 30);
itemRepository.update(itemId, updateParam);
//then
Item findItem = itemRepository.findById(itemId).get();
assertThat(findItem.getItemName()).isEqualTo(updateParam.getItemName());
assertThat(findItem.getPrice()).isEqualTo(updateParam.getPrice());
assertThat(findItem.getQuantity()).isEqualTo(updateParam.getQuantity());
}
@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);
}
void test(String itemName, Integer maxPrice, Item... items) {
List<Item> result = itemRepository.findAll(new ItemSearchCond(itemName, maxPrice));
assertThat(result).containsExactly(items);
}
}
- ItemRepositoryTest는 @SpringBootTest를 사용한다. @SpringBootTest는 @SpringBootApplication를 찾아서 설정으로 사용한다
- @SpringBootApplication 설정이 과거에는 MemoryConfig.class 를 사용하다가 이제는 JdbcTemplateV3 Config.class를 사용하도록 변경되었다. 따라서 테스트도 JdbcTemplate을 통해 실제 데이터베이스를 호출하게 된다.
- MemoryItemRepository -> JdbcTemplateItemRepositoryV3
- save(), updateItem() 은 성공, findItems()는 실패한다.
- 테스트에서 저정한 3개의 데이터가 조회되어야 하는데, 기대보다 더 많은 데이터가 조회되었다.
- 문제는 H2 데이터베이스에 이미 과거에 서버를 실행하면서 저장했던 데이터가 보관되어 있기 때문이다. 이 데이터가 현재 테스트에 영향을 준다
- 테스트는 독립된 환경에서 실행이 되어야 하므로 데이터베이스를 실제 운영환경과 분리하여야 한다.
데이터베이스 분리
로컬에서 사용하는 애플리케이션 서버와 테스트에서 같은 데이터베이스를 사용하고 있으니 테스트에서 문제가 발생한다. 이런 문제를 해결하려면 테스트를 다른 환경과 철저하게 분리해야 한다.
가장 간단한 방법은 테스트 전용 데이터베이스를 별도로 운영하는 것이다. H2 데이터베이스를 용도에 따라 2가지로 구분하면 된다.
- jdbc:h2:tcp://localhost/~/test : local에서 접근하는 서버 전용 데이터베이스
- jdbc:h2:tcp://localhost/~/testcase : test 케이스에서 사용하는 전용 데이터베이스
src/test/resources/application.properties
spring.profiles.active=test
spring.datasource.url=jdbc:h2:tcp://localhost/~/testcase
spring.datasource.username=sa
#jdbcTemplate sql log
logging.level.org.springframework.jdbc=debug
- 접속 정보를 변경한다 ( jdbc:h2:tcp://localhost/~/test -> jdbc:h2:tcp://localhost/~/testcase)
해당 DB에 물론 아래의 스키마를 먼저 실행해 주어야한다
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)
);
- 이제 findItem() 테스트를 단독으로 실행하면, 처음에는 성공한다. 하지만, 다시 실행하면 실패하는 것을 확인할 수 있다.
- WHY?
- 테스트를 2번째 실행할 때 실패하는 이유는 testcase 데이터베이스에 접속해서 item 테이블의 데이터를 확인하면 알 수 있다.
- 처음 테스트를 실행할 때 저장한 데이터가 계속 남아있기 때문에 두 번째 테스트에 영향을 준 것이다.
- 이 문제는 save() 같은 다른 테스트가 먼저 실행되고 나서 findItems()를 실행할 때도 나타난다. 다른 테스트에서 이미 데이터를 추가했기 때문이다. 결과적으로 테스트 데이터가 오염된 것이다.
- 이 문제를 해결하려면 각각의 테스트가 끝날 때마다 해당 테스트에서 추가한 데이터를 삭제해야 한다. 그래야 다른 테스트에 영향을 주지 않는다
- 테스트에서 매우 중요한 원칙은 다음과 같다.
- 테스트는 다른 테스트와 격리해야 한다.
- 테스트는 반복해서 실행할 수 있어야 한다.
- 물론 테스트가 끝날 때 마다 추가한 데이터에 DELETE SQL을 사용해도 되겠지만, 이 방법도 궁극적인 해결책은 아니다. 만약 테스트 과정에서 데이터를 이미 추가했는데, 테스트가 실행되는 도중에 예외가 발생하거나 애플리케이션이 종료되어 버려서 테스트 종료 시점에 DELETE SQL 을 호출하지 못할 수도 있다! 그러면 결국 데이터가 남아있게 된다
- 이를 해결하기 위해서는 트랜잭션 Rollback 기능을 사용하면 된다.
데이터 롤백
테스트가 끝나고 나서 트랜잭션을 강제로 롤백해버리면 데이터가 깔끔하게 제거된다. 테스트를 하면서 데이터를 이미 저장했는데, 중간에 테스트가 실패해서 롤백을 호출하지 못해도 괜찮다. 트랜잭션을 커밋하지 않았기 때문에 데이터베이스에 해당 데이터가 반영되지 않는다. 이렇게 트랜잭션을 활용하면 테스트가 끝나고 나서 데이터를 깔끔하게 원래 상태로 되돌릴 수 있다.
@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);
}
...
}
- 테스트는 각각의 테스트 실행 전 후로 동작하는 @BeforeEach , @AfterEach라는 편리한 기능을 제공한다.
- 트랜잭션 관리자는 PlatformTransactionManager를 주입받아서 사용하면 된다. 참고로 스프링 부트는 자동으로 적절한 트랜잭션 매니저를 스프링 빈으로 등록해준다.
- @BeforeEach : 각각의 테스트 케이스를 실행하기 직전에 호출된다. 따라서 여기서 트랜잭션을 시작하면 된다. 그러면 각각의 테스트를 트랜잭션 범위 안에서 실행할 수 있다.
- transactionManager.getTransaction(new DefaultTransactionDefinition()) 로 트랜잭션을 시작한다
- @AfterEach : 각각의 테스트 케이스가 완료된 직후에 호출된다. 따라서 여기서 트랜잭션을 롤백하면 된다. 그러면 데이터를 트랜잭션 실행 전 상태로 복구할 수 있다.
- transactionManager.rollback(status)로 트랜잭션을 롤백한다.
- 이제 테스트의 데이터가 Transaction 단위로 롤백이 되기 때문에 여러 번 실행하여도 테스트에 영향이 가지 않는다.
728x90
'스프링 DB 2편(데이터 접근 활용 기술)' 카테고리의 다른 글
| Ch04. MyBatis - MyBatis 소개 (0) | 2022.06.27 |
|---|---|
| Ch03. 테스트 - @Transactional, 임베디드 모드 DB, 스프링 부트와 임베디드 모드 (0) | 2022.06.26 |
| Ch02. 스프링 JdbcTemplate - JdbcTemplate 기능 정리 (0) | 2022.06.25 |
| Ch02. 스프링 JdbcTemplate - JdbcTemplate(SimpleJdbcInsert) (0) | 2022.06.25 |
| Ch02. 스프링 JdbcTemplate - JdbcTemplate(이름 지정 파라미터) (0) | 2022.06.25 |