• Spring batch integration test (feat.Elasticsearch)
    DEV 2023. 12. 2. 20:10

    test

    Purpose of Integration Test

    • 여러 컴포넌트 간 상호작용이 정상적으로 수행되는지
    • DAO가 올바르게 연결돼 있어서 원하는 데이터를 저장, 읽을 수 있는지?

    Test Environment Setup

    멱등성(idempotent) 유지

    • 연산을 여러 번 적용하더라도 결과가 달라지지 않는 성질
    • 멱등성이 깨지기 쉬운 구간 → 외부 모듈 → DB
    • 개발 DB로 테스트를 하기엔 좀 그렇고, 테스트만을 위한 DB를 세팅하기도 좀..

    DB 테스트 환경 구축 방법

    1. local DB 설치
    2. in-memory DB
      - H2, 빠르지만 특정 DB에 특화된 기능 지원 x
    3. 사용하는 DB의 Embedded Library 사용
      - in-memory DB 이슈 해결
      - embedded를 지원하지 않거나 특정 버전, OS지원 안 하는 경우
    4. docker compose
      - docker-compose 파일 따로 관리
      - docker 컨테이너와 통신 설정 어려움
    5. TestContainer
      - docker compose와 동작 원리는 같지만 docker-compose 설정 x
      - 코드만으로 docker 컨테이너를 사용한 테스트 환경 설정 컨테이너와 통신 부분도 언어 레벨에서 처리

    ElasticSearch로 테스트 환경을 만들어 보자

    testcontainers

    ElasticSearch TestContainer setup

    Adding dependency

    testImplementation "org.testcontainers:elasticsearch:1.16.3"

    ESTestContainer.class

    @TestConfiguration
    @EnableElasticsearchRepositories(basePackages = "com.k13e.muffin.module.common.repository")
    public class ESTestContainer extends AbstractElasticsearchConfiguration {
    
        private static final DockerImageName ELASTICSEARCH_IMAGE = DockerImageName
                .parse("docker.elastic.co/elasticsearch/elasticsearch")
                .withTag("7.16.2");
        private static final ElasticsearchContainer container;
    
        static {
            container = new ElasticsearchContainer(ELASTICSEARCH_IMAGE)
                    .withExposedPorts(9200,9300)
                    .withEnv("discovery.type","single-node");
            container.start();
        }
    
        @Override
        public RestHighLevelClient elasticsearchClient() {
            String hostAddress = new StringBuilder()
                    .append(container.getHost())
                    .append(":")
                    .append(container.getMappedPort(9200))
                    .toString();
    
            ClientConfiguration clientConfiguration = ClientConfiguration.builder()
                    .connectedTo(hostAddress)
                    .build();
            return RestClients.create(clientConfiguration).rest();
        }
    }

    BatchJobTest.class

    @ExtendWith(SpringExtension.class)
    @ContextConfiguration(classes = {Configuration.class,
            ESTestContainer.class,
    })
    @SpringBatchTest
    public class BatchJobTest {
    ....
    Test repository
    @Import(ESTestContainer.class)
    @SpringBootTest(classes = DataRepository.class)
    public class DataRepositoryTest {
    
        @Autowired
        DataRepository dataRepository;
    
        @Test
        void test() {
            //given
            final Data d = Data.builder()
                    .id("test")
                    .active(true)
                    .build();
    
            dataRepository.save(d);
    
            //when
            final Optional<Data> test = dataRepository.findById("test");
            final Stream<Data> test1 = dataRepository.findDataByActiveIsTrue();
    
            //then
            assertThat(test.get().getId()).isEqualTo(d.getId());
            assertThat(test1.findFirst().get().getId()).isEqualTo(d.getId());
        }
    }

    Test Spring batch

    • Spring batch에서 step scope, job scope → 유용한 도구
    • 하지만 테스트 시 까다로울 수 있다
      - step scope 바깥에서 해당 컴포넌트를 실행했을 때 의존성 문제

    Spring batch component

    batch component
    batch process

    • batch job 시작 → jobInstance 생성 ⇒ jobInstance = jobName + jobParameter
    • 동일한 jobParmeter로 동일한 job을 두 번 이상 실행 할 수 x
    • jobInstance → 잡의 논리적 실행
      - job : jobInstance = 1 : N
    • jobExecution → 잡 실행의 실제 시도
      - jobInstance : jobExecution = 1 : N
    • jobParameter → 배치 작업이 수행될 때마다 전달되는 Parameter
      - ex: dataType, run.id..

    batch flow
    batch flow

    JobScope, StepScope 하는 일

    • @JobScope는 step에 선언가능
    • @StepScope는 itemReader, itemWriter, itemProcessor, tasklet에 사용 가능
    • 선언한 bean의 생성을 지연 (late-binding)
      - controller, service 같은 비즈니스 로직 레벨에서 jobParmeter를 할당할 수 있다.

    jobParameter 주입

    • JobExecution, StepExcution에서 jobParmeter를 가져와 빈에 주입

    그럼 itemReader를 테스트하기 위해서는?

    • 테스트 환경에 StepExecution을 생성 후, 필요한 jobParmeter를 설정
    @ExtendWith(SpringExtension.class)
    @ContextConfiguration(classes = {Configuration.class,
            TestBatchConfig.class,
            ESTestContainer.class
    })
    @SpringBatchTest
    public class ApiItemReaderTest {
        @Autowired
        private DataRepository dataRepository;
        
        @Autowired
        private ItemReader<LinkedHashMap> itemReader;
    
        private String id = "test";
        
        @BeforeEach
        public void setup(){
            dataRepository.save(getData(id));
        }
    
        public StepExecution getStepExecution() {
            JobParameters jobParameters = new JobParametersBuilder()
                    .addString("id", "test")
                    .toJobParameters();
            return MetaDataInstanceFactory.createStepExecution(jobParameters);
        }
    
        @Test
        public void test() throws Exception {
            final Optional<Data> savedData = dataRepository.findById(id);
            assertNotNull(itemReader.read());
            assertEquals(savedData.get().getId(), id);
        }
    }

    @TestExecutionListener

    • 테스트 메서드 실행 전후에 수행돼야 하는 일을 정의하는 스프링 API
    • @BeforeEach, @AfterEach 보다 재사용성이 좋고, 테스트 케이스의 모든 메서드에 원하는 동작을 재사용 가능한 방식으로 삽입 가능

    StepScopeTestExecutionListener

    • 테스트 케이스에서 StepExecution을 가져오고, 콘텍스트를 현재 테스트메서드의 컨텍스트로 사용
    • 모든 테스트 메서드가 실행되는 동안 스텝 컨텍스트를 제공

    Job, Step를 테스트하려면

    • jobLauncherTestUtils.launchJob(jobParameters);
    • jobLauncherTestUtils.launchStep("test", jobParameters);

    # Reference

    - https://docs.spring.io/spring-batch/docs/current/reference/html/index.html

    - https://medium.com/riiid-teamblog-kr/testcontainer-로-멱등성있는-integration-test-환경-구축하기-4a6287551a31

    - https://terasoluna-batch.github.io/guideline/5.0.0.RELEASE/ja/Ch02_SpringBatchArchitecture.html

    728x90

    'DEV' 카테고리의 다른 글

    Spring batch jobScope, stepScope  (0) 2023.12.02
    Elasticsearch에서의 relation  (1) 2023.12.02
    K8S, DNS 간헐적 5~15초 지연  (0) 2023.12.02
    Test Double  (1) 2023.12.02
    Dependency Mechanism  (1) 2023.12.02
go.