스프링 프레임워크/springbatch

springbatch5 h2 database 특정 job 테스트하기

blogger903 2024. 7. 9. 23:12
728x90

이번 포스트에서는 springbatch5에서 h2 database으로 테스트하겠습니다

 

환경

  • springboot 3.3.0
  • gradle 8.8
  • java 17

build.gradle

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.3.0'
    id 'io.spring.dependency-management' version '1.1.5'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-batch'
    implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.mysql:mysql-connector-j'
    testImplementation 'com.h2database:h2'
    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.batch:spring-batch-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
    useJUnitPlatform()
}

 

 

 

테스트하려는 간단한 잡을 생성합니다

chunk 기반 job입니다

import com.example.springbatch.core.domain.PlainText;
import com.example.springbatch.core.domain.ResultText;
import com.example.springbatch.core.repository.PlainTextRepository;
import com.example.springbatch.core.repository.ResultTextRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobScope;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.data.RepositoryItemReader;
import org.springframework.batch.item.data.builder.RepositoryItemReaderBuilder;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.Sort;
import org.springframework.transaction.PlatformTransactionManager;

import java.util.Collections;
import java.util.List;

@Configuration
@RequiredArgsConstructor
public class PlainTextJobConfig {

    private final PlainTextRepository plainTextRepository;
    private final ResultTextRepository resultTextRepository;

    @Bean("plainTextJob")
    public Job plainTextJob(JobRepository jobRepository, Step plainTextStep, Step initStep) {
        return new JobBuilder("plainTextJob", jobRepository)
                .incrementer(new RunIdIncrementer())
                .start(initStep)  // 초기 데이터 삽입을 위한 step
                .next(plainTextStep)
                .build();
    }

    @JobScope
    @Bean("initStep")
    public Step initStep(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
        return new StepBuilder("initStep", jobRepository)
                .tasklet((contribution, chunkContext) -> {
                    plainTextRepository.saveAll(List.of(
                            new PlainText(null, "Text 1"),
                            new PlainText(null, "Text 2"),
                            new PlainText(null, "Text 3")
                    ));
                    return RepeatStatus.FINISHED;
                }, transactionManager)
                .build();
    }

    @JobScope
    @Bean("plainTextStep")
    public Step plainTextStep(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
        return new StepBuilder("plainTextStep", jobRepository)
                .<PlainText, String>chunk(5, transactionManager)
                .reader(plainTextReader())
                .processor(plainTextProcessor())
                .writer(plainTextWriter())
                .build();
    }

    @StepScope
    @Bean
    public RepositoryItemReader<PlainText> plainTextReader() {
        return new RepositoryItemReaderBuilder<PlainText>()
                .name("plainTextReader")
                .repository(plainTextRepository)
                .methodName("findAll")
                .pageSize(5)
                .sorts(Collections.singletonMap("id", Sort.Direction.ASC))
                .build();
    }

    @StepScope
    @Bean
    public ItemProcessor<PlainText, String> plainTextProcessor() {
        return item -> "Processed: " + item.getText();
    }

    @StepScope
    @Bean
    public ItemWriter<String> plainTextWriter() {
        return items -> {
            items.forEach(item -> resultTextRepository.save(new ResultText(null, item)));
            System.out.println("=== chunk is finished ===");
        };
    }
}

 

repository와 entity입니다 

import com.example.springbatch.core.domain.PlainText;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;

public interface PlainTextRepository extends JpaRepository<PlainText, Long> {
    Page<PlainText> findBy(Pageable pageable);
}

 

import com.example.springbatch.core.domain.ResultText;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ResultTextRepository extends JpaRepository<ResultText, Long> {
    Page<ResultText> findBy(Pageable pageable);
}

 

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Setter
@Getter
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "plain_text")
public class PlainText {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "text", nullable = false)
    private String text;
}

 

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Setter
@Getter
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "result_text")
public class ResultText {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "text", nullable = false)
    private String text;
}

 

Job을 테스트합니다

특정 Job을 테스트하기 위해 BatchConfig, TestConfig를 추가했습니다


import com.example.springbatch.core.domain.ResultText;
import com.example.springbatch.core.repository.PlainTextRepository;
import com.example.springbatch.core.repository.ResultTextRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.test.JobLauncherTestUtils;
import org.springframework.batch.test.context.SpringBatchTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;

import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;


@SpringBatchTest
@ActiveProfiles("test")
@ContextConfiguration(classes = {BatchTestConfig.class,PlainTextJobConfig.class, TestConfig.class})
class PlainTextJobConfigTest {
    @Autowired
    private JobLauncherTestUtils jobLauncherTestUtils;

    @Autowired
    private PlainTextRepository plainTextRepository;

    @Autowired
    private ResultTextRepository resultTextRepository;


    @AfterEach
    public void tearDown() {
        plainTextRepository.deleteAll();
        resultTextRepository.deleteAll();
    }

    @ParameterizedTest
    @ValueSource(strings = {"Text 1", "Text 2", "Text 3"})
    public void testEachProcessedText(String expectedText) throws Exception {
        JobExecution jobExecution = jobLauncherTestUtils.launchJob();

        assertEquals(ExitStatus.COMPLETED, jobExecution.getExitStatus());

        List<ResultText> results = resultTextRepository.findAll();
        assertTrue(results.stream()
                .anyMatch(result -> result.getText().equals("Processed: " + expectedText)));
    }
}

 

Springbatch5에서 @EnableBatchProcessing을 사용하지 않아도 되지만, SpringBootApplication 애노테이션을 사용하지 않고

@EnableAutoConfiguration을 사용하다보니, 추가했고, EnableJpaRepositories와 EntityScan도 마찬가지입니다 

import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@Configuration
@EnableBatchProcessing
@EnableJpaRepositories("com.example.springbatch.core.repository")
@EntityScan("com.example.springbatch.core.domain")
@EnableAutoConfiguration
public class BatchTestConfig {
}

 

SpringBatch metadata 테이블이 없으면 실행이 안되기 때문에 DataSource를 구현합니다
H2DB를 생성하고 metadata 테이블을 생성하기 위한 스크립트를 실행합니다

import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;

import javax.sql.DataSource;

@TestConfiguration
class TestConfig {
    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .addScript("classpath:org/springframework/batch/core/schema-h2.sql")
                .build();
    }
}

 

application.yml입니다

ddl-auto를 update로 맞춰줍니다

spring:
  application:
    name: spring-batch
  profiles:
    active: local
  batch:
    job:
      name: ${job.names:NONE}
      
---
spring:
  config:
    activate:
      on-profile: test
  jpa:
    show-sql: true
    database-platform: org.hibernate.dialect.H2Dialect
    hibernate:
      ddl-auto: update
  batch:
    jdbc:
      initialize-schema: always