3 분 소요


테스트 - 데이터베이스 연동

테스트 케이스에서도 데이터베이스에 접속할 수 있게 test의 application.properties를 수정했다.

src/test/resources/application.propertes

spring.profiles.active=test
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa

테스트 실행 - 로컬DB

@SpringBootTest

@SpringBootTest
class ItemRepositoryTest {}
  • ItemRepositoryTest@SpringBootTest를 사용한다.
  • @SpringBootTest@SpringBootApplication을 찾아서 설정으로 사용한다.

실행 결과

  • updateItem(): 성공
  • save(): 성공
  • findItems(): 실패

findItems()는 상품을 3개 저장하고, 조회한다.

@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);

    //여기서 3개 이상이 조회되는 문제가 발생
    test(null, null, item1, item2, item3);
}

결과적으로 테스트에서 저장한 3개의 데이터가 조회 되어야 하는데, 기대보다 더 많은 데이터가 조회되었다.

실패 원인

문제는 H2 데이터베이스에 이미 과거에 서버를 실행하면서 저장했던 데이터가 보관되어 있기 때문이다. 이 데이터가 현재 테스트에 영향을 준다.


데이터베이스 분리

로컬에서 사용하는 애플리케이션 서버와 테스트에서 같은 데이터베이스를 사용하고 있기 때문에 문제가 발생한다.
이런 문제를 해결하려면 테스트를 다른 환경과 분리해야 한다.

가장 간단한 방법은 테스트 전용 데이터베이스를 별도로 운영하는 것이다.

  • H2 데이터베이스를 용도에 따라 2가지로 구분
    • jdbc:h2:tcp://localhost/~/test : local에서 접근하는 서버 전용 데이터베이스
    • jdbc:h2:tcp://localhost/~/testcase : test 케이스에서 사용하는 전용 데이터베이스

test-application.properties
src/test/resources/application.properties

spring.profiles.active=test
spring.datasource.url=jdbc:h2:tcp://localhost/~/testcase
spring.datasource.username=sa

테스트 실행

  • findItems() 테스트 실행
    • 처음에는 실행에 성공
    • 하지만 같은 테스트를 다시 실행하면 테스트에 실패한다.
  • 2번째 테스트부터 실패하는 이유는 testcase 데이터베이스에 처음 테스트를 실행할 때 저장한 데이터가 계속 남아있기 때문이다.
  • 이 문제를 해결하려면 각각의 테스트가 끝날 때 마다 해당 테스트에서 추가한 데이터를 삭제해야 한다.

테스트에서 중요한 원칙

  • 테스트는 다른 테스트와 격리해야 한다.
  • 테스트는 반복해서 실행할 수 있어야 한다.


데이터 롤백

트랜잭션과 롤백 전략
데이터베이스에 데이터가 남아있는 문제를 해결할 수 있는것이 트랜잭션이다.
테스트가 끝나고 나서 트랜잭션을 강제로 롤백해버리면 데이터가 깔끔하게 제거된다.
테스트를 하면서 데이터를 이미 저장했는데, 중간에 테스트가 실패해서 롤백을 호출하지 못해도 괜찮은 것이, 트랜잭션을 커밋하지 않았기 때문에 데이터베이스에 해당 데이터가 반영되지 않는다.

1. 트랜잭션 시작
2. 테스트 A 실행
3. 트랜잭션 롤백

4. 트랜잭션 시작
5. 테스트 B 실행
6. 트랜잭션 롤백

테스트는 각각의 테스트 실행 전 후로 동작하는 @BeforeEach, @AfterEach라는 편리한 기능을 제공한다.

직접 트랜잭션 추가

@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);
    }
}
  • 트랜잭션 관리자는 PlatformTransactionManager를 주입 받아서 사용하면 된다.
  • @BeforeEach: 각각의 테스트 케이스를 실행하기 직전에 호출된다.
    • 따라서, 여기서 트랜잭션을 시작하면 각각의 테스트를 트랜잭션 범위 안에서 실행할 수 있다.
  • @AfterEach: 각각의 테스트 케이스가 완료된 직후에 호출된다.
    • 따라서, 여기서 트랜잭션을 롤백하면 데이터를 트랜잭션 실행 전 상태로 복구할 수 있다.
    • transactionManager.rollback(status)


@Transactional

스프링은 테스트 데이터 초기화를 위해 트랜잭션을 적용하고 롤백하는 방식을 @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이 테스트에 있으면 스프링은 테스트를 트랜잭션 안에서 실행하고, 테스트가 끝나면 트랜잭션을 자동으로 롤백시킨다.

테스트 동작 방식

findItems()를 예시로 들었다.

1

  1. 테스트에 @Transactional 애노테이션이 먼저 트랜잭션을 시작한다.
  2. 테스트 로직을 실행하고, 테스트가 끝날 때 까지 모든 로직은 트랜잭션 안에서 수행된다.
    • 트랜잭션은 기본적으로 전파되기 때문에, Repository에서 사용하는 JdbcTemplate도 같은 트랜잭션을 사용한다.
  3. 테스트 실행 중에 INSERT SQL을 사용해서 item1, item2, item3를 데이터베이스에 저장한다.
  4. 검증을 위해 SELECT SQL로 데이터를 조회한다. 여기서는 앞서 저장한 item1, item2, item3이 조회되었다.
    • SELECT SQL도 같은 트랜잭션을 사용하기 때문에 저장한 데이터를 조회할 수 있다.(Commit 전)
      • 다른 트랜잭션에서는 해당 데이터 확인 X
  5. @Transactional이 테스트에 있으면 테스트가 끝날 때 트랜잭션을 강제로 롤백한다.
  6. 롤백에 의해 앞서 데이터베이스에 저장한 item1, item2, item3의 데이터가 제거된다.

[참고]
테스트 케이스의 메서드나 클래스에 @Transactional을 직접 붙여서 사용할 경우에만 이렇게 동작한다.
그리고 트랜잭션을 테스트에서 시작하기 때문에 서비스, 리포지토리에 있는 @Transactional도 테스트에서 시작한 트랜잭션에 참여한다.(테스트 실행이 종료될 때 까지 테스트가 실행하는 모든 코드가 같은 트랜 트랜잭션 범위에 들어간다고 이해하면 된다.)

정리

  • 테스트가 끝난 후 개발자가 직접 데이터를 삭제하지 않아도 되는 편리함을 제공한다.
  • 테스트 실행 중에 데이터를 등록하고 중간에 테스트가 강제로 종료되어도 걱정이 없다.
    • 이 경우 트랜잭션을 커밋하지 않기 때문에, 데이터는 자동으로 롤백된다.
    • 보통 데이터베이스 커넥션이 끊어지면 자동으로 롤백되어 버린다.
  • 트랜잭션 범위 안에서 테스트를 진행하기 때문에 동시에 다른 테스트가 진행되어도 서로 영향을 주지 않는 장점이 있다.
  • @Transactional 덕분에 편리하게 다음 원칙을 지킬 수 있게 되었다.
    • 테스트는 다른 테스트와 격리해야 한다.
    • 테스트는 반복해서 실행할 수 있어야 한다.


<출처 : 인프런 - 스프링 DB 2편 : 데이터 접근 활용 기술(김영한)>

댓글남기기