Programming/Spring

Spring - 데이터 접근 기술 - 테스트

잇(IT) 2023. 7. 3. 23:47

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

 

- main - application.properties

src/main/resources/application.properties
spring.profiles.active=local
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa

logging.level.org.springframework.jdbc=debug

 

- test - application.properties

test - application.properties
spring.profiles.active=test

- 테스트 케이스의 경우 src/test에 있는 application.properties 파일이 우선순위를 가지고 실행된다. 하지만 test의 properties에는 데이터베이스 연결 설정이 없다.

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

logging.level.org.springframework.jdbc=debug

- 위와 같이 test의 properties의 내용을 수정한다.


- @SpringBootTest

@SpringBootTest
class ItemServiceApplicationTests {

- @SpringBootTest는 @SpringBootApplication을 찾아서 설정으로 사용한다.

- @SpringBootTest의 경우 실제 데이터베이스에 저장된 데이터들을 가져온다.

 

- 실제 DB와 연결된 상태에서 Test 코드를 실행하게 되면 실제 DB에 저장된 내용들이 반영되면서 정상적인 Test 수행이 불가능해진다.


- 테스트 - 데이터베이스 분리

 

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

- Test용 DB를 새롭게 하나 생성한다. (H2 DB를 이용하여 새로운 데이터베이스 생성)

 

- test - application.properties

src/test/resources/application.properties
spring.profiles.active=test
spring.datasource.url=jdbc:h2:tcp://localhost/~/testcase
spring.datasource.username=sa
spring.datasource.password=1513

- test의 properties의 내용을 위와 같이 설정해준다. (새롭게 생성한 DB와 연결하기 위한 설정이다.)

 

- ItemRepositoryTest

@SpringBootTest
class ItemRepositoryTest {

    @Autowired
    ItemRepository itemRepository;
    
    .....
    
    @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);
    }

- 테스를 여러번 실행하게 되면 테스트에 실패한다.

- 처음 테스트를 실행할 때 저장한 데이터가 계속해서 남아있기 때문이다.


- 테스트에서 매우 중요한 원칙은 다음과 같다.

1. 테스트는 다른 테스트와 격리해야 한다.

2. 테스트는 반복해서 실행할 수 있어야 한다.


- 테스트 - 데이터 롤백

 

- 트랜잭션과 롤백이 도움이 된다. 테스트가 끝나고 트랜잭션을 롤백하게 되면 데이터가 깔끔하게 제거된다.

- 커밋을 하지 않았기 때문에 데이터베이스에 데이터가 반영되지 않는다.

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

1. 트랜잭션 관리자는 PlatformTransactionManager 를 주입 받아서 사용하면 된다. 참고로 스프링 부트는 자동으로 적절한 트랜잭션 매니저를 스프링 빈으로 등록해준다. (앞서 학습한 스프링 부트의 자동 리소스 등록 장을 떠올려보자.)

2. @BeforeEach : 각각의 테스트 케이스를 실행하기 직전에 호출된다. 따라서 여기서 트랜잭션을 시작하면 된다. 그러면 각각의 테스트를 트랜잭션 범위 안에서 실행할 수 있다.

   2.1. transactionManager.getTransaction(new DefaultTransactionDefinition()) 로 트랜잭션을 시작한다.

3. @AfterEach : 각각의 테스트 케이스가 완료된 직후에 호출된다. 따라서 여기서 트랜잭션을 롤백하면 된다. 그러면 데이터를 트랜잭션 실행 전 상태로 복구할 수 있다.

   3.1. 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 원리

1. 스프링이 제공하는 @Transactional 애노테이션은 로직이 성공적으로 수행되면 커밋하도록 동작한다.

2. 그런데 @Transactional 애노테이션을 테스트에서 사용하면 아주 특별하게 동작한다.

3. @Transactional 이 테스트에 있으면 스프링은 테스트를 트랜잭션 안에서 실행하고, 테스트가 끝나면 트랜잭션을 자동으로 롤백시켜 버린다.

 

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

1. 테스트에 @Transactional 애노테이션이 테스트 메서드나 클래스에 있으면 먼저 트랜잭션을 시작한다.

2. 테스트를 로직을 실행한다. 테스트가 끝날 때 까지 모든 로직은 트랜잭션 안에서 수행된다. 트래잭션은 기본적으로 전파되기 때문에, 리포지토리에서 사용하는 JdbcTemplate도 같은 트랜잭션을 사용한다.

3. 테스트 실행 중에 INSERT SQL을 사용해서 item1 , item2 , item3 를 데이터베이스에 저장한다. 물론 테스트가 리포지토리를 호출하고, 리포지토리는 JdbcTemplate을 사용해서 데이터를 저장한다.

4. 검증을 위해서 SELECT SQL로 데이터를 조회한다. 여기서는 앞서 저장한 item1 , item2 , item3 이 조회되었다. SELECT SQL도 같은 트랜잭션을 사용하기 때문에 저장한 데이터를 조회할 수 있다. 다른 트랜잭션에서는 해당 데이터를 확인할 수 없다. 여기서 assertThat() 으로 검증이 모두 끝난다.

5. @Transactional 이 테스트에 있으면 테스트가 끝날때 트랜잭션을 강제로 롤백한다.

6. 롤백에 의해 앞서 데이터베이스에 저장한 item1 , item2 , item3 의 데이터가 제거된다.


- 정리

1. 테스트가 끝난 후 개발자가 직접 데이터를 삭제하지 않아도 되는 편리함을 제공한다.

2. 테스트 실행 중에 데이터를 등록하고 중간에 테스트가 강제로 종료되어도 걱정이 없다. 이 경우 트랜잭션을 커밋하지 않기 때문에, 데이터는 자동으로 롤백된다. (보통 데이터베이스 커넥션이 끊어지면 자동으로 롤백되어 버린다.)

3. 트랜잭션 범위 안에서 테스트를 진행하기 때문에 동시에 다른 테스트가 진행되어도 서로 영향을 주지 않는 장점이 있다. 4. @Transactional 덕분에 아주 편리하게 다음 원칙을 지킬수 있게 되었다.

   4.1. 테스트는 다른 테스트와 격리해야 한다.

   4.2. 테스트는 반복해서 실행할 수 있어야 한다.


- 강제로 커밋하기 - @Commit

@Commit
@Transactional
@SpringBootTest
class ItemRepositoryTest {}

- 위와 같이 @Commit 혹은 @Rollback(value = false)을 사용하게 되면 rollback이 되는것이 아니라 커밋을 하게된다.


- 테스트 - 임베디드 모드 DB

 

- 임베디드 모드

1. H2 데이터베이스는 자바로 개발되어 있고, JVM안에서 메모리 모드로 동작하는 특별한 기능을 제공한다. 그래서 애플리케이션을 실행할 때 H2 데이터베이스도 해당 JVM 메모리에 포함해서 함께 실행할 수 있다. DB를 애플리케이션에 내장해서 함께 실행한다고 해서 임베디드 모드(Embedded mode)라 한다. 물론 애플리케이션이 종료되면 임베디드 모드로 동작하는 H2 데이터베이스도 함께 종료되고, 데이터도 모두 사라진다. 쉽게 이야기해서 애플리케이션에서 자바 메모리를 함께 사용하는 라이브러리처럼 동작하는 것이다

@Slf4j
//@Import(MemoryConfig.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("1513");
		return dataSource;
	}
}

1. @Profile("test")

   1.1. 프로필이 test 인 경우에만 데이터소스를 스프링 빈으로 등록한다.

   1.2. 테스트 케이스에서만 이 데이터소스를 스프링 빈으로 등록해서 사용하겠다는 뜻이다.

2. dataSource()

   2.1. jdbc:h2:mem:db : 이 부분이 중요하다. 데이터소스를 만들때 이렇게만 적으면 임베디드 모드(메모리 모드)로 동작하는 H2 데이터베이스를 사용할 수 있다.

   2.2. DB_CLOSE_DELAY=-1 : 임베디드 모드에서는 데이터베이스 커넥션 연결이 모두 끊어지면 데이터베이스도 종료되는데, 그것을 방지하는 설정이다.

   2.3. 이 데이터소스를 사용하면 메모리 DB를 사용할 수 있다.

 

- 기존의 DB는 꺼야 메모리 DB가 생성되고 실행된다.

 

- 메모리 DB에는 Table이 없기 때문에 생성해주지 않으면 오류가 발생한다.


- 스프링 부트 - 기본 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)
);

- 위와같이 파일을 하나 생성하여 SQL 코드를 작성해준다.


- 테스트 - 스프링 부트와 임베디드 모드

 

@Slf4j
//@Import(MemoryConfig.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("1513");
		return dataSource;
	}*/
}

 

- src/test/resources/application.properties

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

- 위와 같이 DB 관련 설정을 전부 주석처리하게 되면 DB 관련 정보가 하나도 없는 것을 알 수 있다.

- 하지만 테스트를 실행하면 정상적으로 실행되는 것을 확인할 수 있다.

 

conn0: url=jdbc:h2:mem:d8fb3a29-caf7-4b37-9b6c-b0eed9985454

- 로그를 확인해보면 스프링 부트에서 메모리 DB를 생성하여 사용한 것을 확인 할 수 있다.

 

즉, DB 관련 설정이 없으면 스프링 부트에서 메모리 DB를 생성하여 데이터를 관리한다는 것을 알 수 있다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

출처 : 인프런 - 우아한 형제들 기술이사 김영한의 스프링 완전 정복 (스프링 DB 2편 - 데이터 접근 활용 기술)

728x90