TIL

Spring Batch Testing

테스트 환경 구축

의존성

테스트 전용 설정 (application-test.yml)

spring:
  datasource:
    url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
    driver-class-name: org.h2.Driver
  sql:
    init:
      mode: always
      schema-locations: classpath:schema.sql
  batch:
    jdbc:
      initialize-schema: always
    job:
      enabled: false   # JobLauncherApplicationRunner 자동 실행 방지

프로젝트 구조

src/
├── main/
│   ├── java/
│   └── resources/
│       └── application.yml
└── test/
    ├── java/
    └── resources/
        ├── application-test.yml
        └── schema.sql          # 테스트용 테이블 정의

E2E 테스트

@SpringBatchTest

@SpringBatchTest
@SpringBootTest
@ActiveProfiles("test")
class BatchJobTest {
    @Autowired
    private JobLauncherTestUtils jobLauncherTestUtils; // 자동 주입
}

JobLauncherTestUtils

Job 지정

@PostConstruct
public void init() {
    jobLauncherTestUtils.setJob(myBatchJob); // 명시적 Job 지정
}

전체 Job 실행: launchJob()

JobParameters params = jobLauncherTestUtils.getUniqueJobParametersBuilder()
        .addString("filePath", tempDir.toString())
        .toJobParameters();

JobExecution jobExecution = jobLauncherTestUtils.launchJob(params);
assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED);

개별 Step 실행: launchStep()

// Step만 실행 (JobParameters 직접 전달)
JobExecution jobExecution =
        jobLauncherTestUtils.launchStep("brainwashStep", jobParameters);

// ExecutionContext로 이전 Step 데이터 시뮬레이션 (유니크 파라미터 자동 생성)
ExecutionContext ctx = new ExecutionContext();
ctx.putLong("victimCount", 4L);
JobExecution jobExecution =
        jobLauncherTestUtils.launchStep("statisticsStep", ctx);

ExecutionContextTestUtils

// Job ExecutionContext에서 값 추출
Long count = ExecutionContextTestUtils.getValueFromJob(jobExecution, "victimCount");

// 특정 Step의 ExecutionContext에서 값 추출
Double rate = ExecutionContextTestUtils.getValueFromStep(stepExecution, "successRate");

// Job 내 특정 Step 이름으로 값 추출
Long count = ExecutionContextTestUtils.getValueFromStepInJob(jobExecution, "stepName", "key");

단위 테스트

@StepScope 빈 테스트의 문제

@Bean
@StepScope
public FlatFileItemWriter<Victim> victimWriter(
        @Value("#{jobParameters['filePath']}") String filePath) { ... }
// Step 실행 없이 이 빈을 어떻게 테스트할 것인가?

StepScopeTestExecutionListener

getStepExecution()으로 커스텀 StepExecution 생성

// 메서드 이름과 반환 타입만 맞추면 StepScopeTestExecutionListener가 자동 탐지
public StepExecution getStepExecution() {
    JobParameters params = new JobParametersBuilder()
            .addString("filePath", "/test/path")
            .addLong("random", new SecureRandom().nextLong())
            .toJobParameters();
    return MetaDataInstanceFactory.createStepExecution(params);
}

getStepExecution() 방식의 한계

StepScopeTestUtils

// 테스트 메서드 내에서 직접 StepExecution 생성 → @TempDir, 메서드별 파라미터 모두 사용 가능
StepExecution stepExecution = MetaDataInstanceFactory.createStepExecution(jobParameters);

StepScopeTestUtils.doInStepScope(stepExecution, () -> {
    writer.open(new ExecutionContext());
    writer.write(new Chunk<>(items));
    writer.close();
    return null;
});

// ItemReader 테스트 — Callable이 결과를 반환할 수 있다
Victim result = StepScopeTestUtils.doInStepScope(
        stepExecution, () -> reader.read());

단위 테스트 시 ItemStream 주의사항