framework/spring

3. 데이터 접근 기술 - 테스트

wooweee 2023. 9. 25. 16:39
728x90

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

  • 데이터 접근 기술은 실제 데이터베이스에 접근해서 데이터를 잘 저장하고 조회할 수 있는지 확인하는 과정이 필요
  • test case는 src/test에 있기 때문에, test 실행시 src/test 내부 application.properties 파일이 우선순위를 가지고 실행
  • 해당 설정 파일에도 spring.datasource.url 같은 db 연결 설정 필요

  • 만약 main의 application.properties의 설정을 그대로 사용하고 싶으면 test/resources 패키지에 application.properties 파일 자체가 존재하지 않아야 사용 가능

 

1.1. h2 db datasource 설정 등록

  • test/resources/application.properties
spring.profiles.active=test

# db 연결
spring.datasource.url=jdbc:h2:tcp://localhost/~/test # 동일한 h2 db 사용
spring.datasource.username=sa
# password 등록 안했을 시, 생략 가능

# sql문 log 확인 설정
logging.level.org.springframework.jdbc=debug



1.2. @SpringBootTest

  • @SpringBootTest는 @SpringBootApplication을 찾아서 springContainer 설정으로 사용

  • @SpringBootApplication 이 존재하는 class의 @Import 또한 주입 받는다.

 

package hello.itemservice;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest 
// main의 @SpringBootApplication 존재하는 class를 찾아서 동일하게 동작
class ItemServiceApplicationTests {
   @Test
   void contextLoads() {
   }
}

 

 

2. test - database 분리

  • 문제
    • local에서 사용하는 appServer와 test에서 같은 db를 사용하고 있으니 test에서 문제가 발생
    • 해당 문제를 해결하기 위해서 test를 다른 환경과 철저하게 분리

  • 해결 방안
    1. 테스트 전용 db를 별도로 운영
    2. h2 같은 경우 인베디드memory db를 이용 (springboot default 설정 값 )

 

2.1. 테스트 전용 db 운영

  • H2 데이터베이스 용도에 따라 2가지로 구분
    • 서버용 db : jdbc:h2:tcp://localhost/~/test
    • test용 db : jdbc:h2:tcp://localhost/~/testcase

 

  • 데이터베이스 파일 생성 방법
    1. db 서버 완전히 종료 후 재 시작
    2. jdbc url : jdbc:h2:~/testcase (최초 한번) - schema 생성 과정이라 보면 된다.
    3. 파일 생성 확인 
      • ~ 경로에서 ls -arlth -> testcase.mv.db 파일 생성 확인
    4. jdbc url : jdbc:h2:tcp://localhost/~/testcase 접속 - schema를 생성 했으므로 local 경로로 실제 db server 접근 필요
spring.profiles.active=test

# db 연결 - test -> testcase 변경
spring.datasource.url=jdbc:h2:tcp://localhost/~/testcase
spring.datasource.username=sa

logging.level.org.springframework.jdbc=debug

 

  • 남은 문제점
    • save로 인해서 저장된 data로 반복되는 test 수행 불가
  • 해결 방안
    • transaction 원리를 이용

 

2.2. test 중요 원칙 

  1. test는 다른 test와 격리해야 한다. - db 분리, 인베디드 db 사용
  2. test는 반복해서 실행할 수 있어야 한다. - clear(), @Transactional

 

 

3.  test - data rollback

  • spring db 1 에서 학습한 transactionManger과 status를 이용하여 status가 true, false 상관 없이 무조건 rollback하도록 @BeforeEach @AfterEach 사용

  • @BeforeEach 수행 부터 @AfterEach 오기전까지 동일 connection을 사용하는 transaction 수행됨
package hello.itemservice.domain;

@SpringBootTest
class ItemRepositoryTest {
    
    // DataSource와 txManger는 springboot에서 자동으로 생성해놓았다.
    @Autowired
    ItemRepository itemRepository;
    
    //tx 관련 code
    @Autowired
    PlatformTransactionManager transactionManager; // spring boot에서 생성해줌 
    TransactionStatus status; // status를 통해서 commit, rollback 수행

    @BeforeEach
    void beforeEach(){
        // tx 시작
        status = transactionManager.getTransaction(new DefaultTransactionDefinition());
    }

    @AfterEach
    void afterEach() {
        //MemoryItemRepository 의 경우 제한적으로 사용
        if (itemRepository instanceof MemoryItemRepository) {
            ((MemoryItemRepository) itemRepository).clearStore();
        }
        // tx rollback
        transactionManager.rollback(status);
    }

    ...
    @Test
}

 

4. test - @Transactional

  • test에서 사용시, 조금 특별하게 사용

  • test 내부에서 @Transactional이 존재시, spring은 transaction 내부에 test를 실행하고, test 종료시 로직의 성공적인 수행여부와 관계 없이 transaction을 항상 rollback 시킴

  • test에 사용되는 @Transactional도 Class, method 모두 원하는 부분에 적용 가능

  •  참고 - @Service test 시, @Service 내부에 @Transactional도 존재할 텐데 어떻게 충돌을 막을까?
    • test case의 @Transactional 과 @Service, @Repository에 있는 @Transactional 도 Test에서 시작한 트랜잭션에 참여
      = Test가 종료될 때까지 Test의 connection을 사용한다는 의미
  • test의 @Transactional 강제 commit 하기
    • @Commit
    • @Rollback(value = false)

 

package hello.itemservice.domain;

// @Transactional, @Commit,@Rollback(value = false) 모두 class, method 영역에 설정 가능
@Transactional
@Commit == @Rollback(value = false)
@SpringBootTest
class ItemRepositoryTest {
    
    // DataSource와 txManger는 springboot에서 자동으로 생성해놓았다.
    @Autowired
    ItemRepository itemRepository;
    
    //tx 관련 code
   /* 
   @Autowired
    PlatformTransactionManager transactionManager;
    TransactionStatus status;

    @BeforeEach
    void beforeEach(){
        // tx 시작
        status = transactionManager.getTransaction(new DefaultTransactionDefinition());
    }
    */

    @AfterEach
    void afterEach() {
        // MemoryItemRepository 의 경우 제한적으로 사용
        if (itemRepository instanceof MemoryItemRepository) {
            ((MemoryItemRepository) itemRepository).clearStore();
        }
        // tx rollback
        // transactionManager.rollback(status);
    }

 // @Rollback(value = false) == @Commit
    @Test

 

5. test - 임베디드 모드 db

 

  • 사용 이유
    1. test case를 실행하려고 별도의 db를 설치하고, 운영하는 것이 비효율적
    2. 단순 검증 용도로 사용하므로 test 종료시 db의 data를 모두 삭제해도 된다.
    3. 더해서 test 종료시, db 자체를 날려도 된다.
  • 임베디드 모드
    • H2 db는 java로 개발 되어있고 JVM 안에서 memory 모드로 동작한느 특별한 기능 제공
    • app 실행할 때 H2 데이터베이스도 해당 JVM 메모리에 포함해서 함께 실행 하 수 있다.
    • db를 app에 내장해서 함께 실행한다고 Embedded mode라고 한다.
    • app 종료시, embedded mode로 동장하는 H2 db도 함께 종료되고 data도 모두 사라진다. 
      = java libarary처럼 동작한다.
  • 수동, 자동 공통 적용 부분
    1. db database 관련 설정 등록 하면 안된다.
    2. main/resources/schema.sql 파일에 table 생성 sql 등록이 필요하다.

 

5.1. 메모리 db용 sql 파일 생성 (공통)

  • embedded 용 db기 때문에 매번 사라졌다 생기므로 table 자체가 저장 될 수 없다.
  • repository에 ddl 로 table을 넣을 수도 있지만 번거롭다.
  • 조금 더 편리한 emebedded(=memory) db 용 sql 파일을 생성

  • 경로 : main/resources/schema.sql
-- memory h2는 현재 table이 생성되어있지 않는 상태
-- 해당 file에서 table을 생성 해줄 것
-- 규칙이니깐 걍 따를 것
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)
);

 

5.2. application.properties database 설정 (공통)

  • 우선 순위 : @SpringBootApplication 내부 @Bean 수동 등록 > @application.properties 수동등록 > 자동등록

  • 수동 모드일 경우는 결국 최상단의 @SpringBootApplication의 main method 내부에서 @Bean을 등록하기 때문에 application.properties의 database 등록이 덮어지지만
  • 자동 모드일 경우, embedded 모드가 수행되지 않게 된다. application.properties에서 database를 수동으로 등록했기 때문

  • 결론 :  헷갈리지 않게 embedded 모드 쓸꺼면 application.properties에서 database 등록 하지 말 것 
spring.profiles.active=test

# db 연결 - test -> testcase 변경
# spring.datasource.url=jdbc:h2:tcp://localhost/~/testcase
# spring.datasource.username=sa

logging.level.org.springframework.jdbc=debug

 

5.3. 임베디드 모드 직접 사용

 

5.3.1. application Class

package hello.itemservice;

@Slf4j
@Import(JdbcTemplateV3Config.class)
@SpringBootApplication(scanBasePackages = "hello.itemservice.web")
public class ItemServiceApplication {
   public static void main(String[] args) {
      SpringApplication.run(ItemServiceApplication.class, args);
   }
   // application.properties의 설정 중 local 환경일 경우 @Bean으로 등록한다.
   @Bean
   @Profile("local")
   public TestDataInit testDataInit(ItemRepository itemRepository) {
      return new TestDataInit(itemRepository);
   }
   @Bean
   @Profile("test")
   public DataSource dataSource() {
      log.info("memory database init");
      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;
   }
}

 

  • embedded용 datasource 및 Driver @Bean 등록 - 핵심 
@Bean
@Profile("test")
public DataSource dataSource() {
  log.info("memory database init");
  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") : 프로필이 test일 경우에만 해당 class를 스프링 빈으로 등록
  • jdbc:h2:mem:db : 데이터소스를 만들때 이렇게만 적으면 임베디드 모드로 동작하는 h2 사용가능
  • DB_CLOSE_DELAY=-1 : 임베디드 모드에서 db connection 연결이 모두 끊어지면 db도 종료되는데, 그것을 방지하는 설정
  • datasource와 driver만 잘 설정하면 된다.

 

5.4. test - springboot와 임베디드 모드

 

  • spring boot는 @Bean 등록과정을 자동으로 처리
  • @Bean 제거
  • 수동 모드와 동일하게 작동
package hello.itemservice;

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

 

6. Test / main Profile 실험

  • application.properties == AP라고 표현

 

  1. test 패키지 경로에 AP file 자체가 없으면 main의 AP를 properties로 사용
    1. main AP에 datasource 등록이 되어있으면 해당 db를 이용해서 test 수행
    2. main AP에 datasource 등록 없으면 embedded mode db 수행

  2. test 패키지 경로에 AP file 이 있는 경우
    - 내부에 profile 설정 여부와 관계없이 해당 test AP를 이용해서 springboot test 수행
    1. test AP에 datasource 등록이 되어있으면 해당 db를 이용해서 test 수행
    2. test AP에 datasource 등록 없으면 embedded mode db 수행

  3. test class에 @Transactional 존재 여부시, 달라지는 log
    - mem이 나오는 것이 확실이 embedded mode임을 나타내는데 2번의 경우도 db를 실행안해도 test가 정상 동작하므로 embedded 모드라고 추정할 수 있다.

    1. @Transactional 존재 시,
      > [HikariProxyConnection@1631143060 wrapping conn0: url=jdbc:h2:mem:b175c7c8-3ee4-4c58-9773-f0be5b88066d user=SA]

    2. @Transactional 존재 안할 시,
      > com.h2database/h2/1.4.200/f7533fe7cb8e99c87a43d325a77b4b678ad9031a/h2-1.4.200