spring-boot-starter-test: 기본 Spring Boot 테스트spring-batch-test: Spring Batch 전용 테스트 도구 — 이것 없이는 배치 테스트가 극히 불편하다h2: 테스트 전용 인메모리 DBspring:
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 자동 실행 방지
spring.batch.job.enabled: false가 핵심 — JobLauncherApplicationRunner는 이 값이 true(기본값)일 때 자동 구성되어 애플리케이션 시작 시 Job을 자동 실행한다JobLauncherTestUtils로 직접 Job을 실행할 것이므로 자동 실행을 꺼야 한다src/
├── main/
│ ├── java/
│ └── resources/
│ └── application.yml
└── test/
├── java/
└── resources/
├── application-test.yml
└── schema.sql # 테스트용 테이블 정의
Job, Step, JobRepository 등 프레임워크 컴포넌트와 우리 코드가 깊게 엮여 동작하므로, 웹 애플리케이션 대비 E2E 테스트의 중요도가 상대적으로 높다JobLauncherTestUtils: Job/Step 실행 유틸리티JobRepositoryTestUtils: JobRepository 관련 유틸리티 (배치 인프라 테스트용, 일반적인 로직 테스트에서는 거의 사용하지 않음)StepScopeTestExecutionListener / JobScopeTestExecutionListener: 배치 스코프 빈 테스트 지원 (단위 테스트에서 상세히 다룸)@SpringBatchTest
@SpringBootTest
@ActiveProfiles("test")
class BatchJobTest {
@Autowired
private JobLauncherTestUtils jobLauncherTestUtils; // 자동 주입
}
JobLauncherTestUtils에 등록된다
BatchTestContextBeanPostProcessor가 ifUnique()로 체크하여 setJob() 호출@PostConstruct
public void init() {
jobLauncherTestUtils.setJob(myBatchJob); // 명시적 Job 지정
}
JobExecution을 반환받는다getUniqueJobParametersBuilder()로 random 키의 랜덤 파라미터를 자동 생성하여 JobInstance 중복을 회피할 수 있다JobParameters params = jobLauncherTestUtils.getUniqueJobParametersBuilder()
.addString("filePath", tempDir.toString())
.toJobParameters();
JobExecution jobExecution = jobLauncherTestUtils.launchJob(params);
assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED);
StepRunner가 TestJob이라는 임시 Job을 생성하고, 해당 Step만 설정하여 실행한다 — 로그에 TestJob이 나타나는 이유ExecutionContext를 파라미터로 전달하여 이전 Step에서 넘어올 데이터를 시뮬레이션할 수 있다
getUniqueJobParameters()를 자동 호출하므로 별도 유니크 파라미터 생성이 불필요// Step만 실행 (JobParameters 직접 전달)
JobExecution jobExecution =
jobLauncherTestUtils.launchStep("brainwashStep", jobParameters);
// ExecutionContext로 이전 Step 데이터 시뮬레이션 (유니크 파라미터 자동 생성)
ExecutionContext ctx = new ExecutionContext();
ctx.putLong("victimCount", 4L);
JobExecution jobExecution =
jobLauncherTestUtils.launchStep("statisticsStep", ctx);
ExecutionContext에 저장된 값을 간편하게 꺼내 검증할 수 있는 유틸리티jobExecution.getExecutionContext().get()으로 직접 접근해도 되지만, Step 이름으로 특정 Step의 컨텍스트를 조회하는 등 편의 메서드를 제공한다// 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");
@Autowired로 빈을 주입받아 바로 테스트하면 된다@StepScope 빈은 Step 실행 시점에 생성되므로, 실제 Step 없이는 빈이 초기화되지 않는다
@Bean
@StepScope
public FlatFileItemWriter<Victim> victimWriter(
@Value("#{jobParameters['filePath']}") String filePath) { ... }
// Step 실행 없이 이 빈을 어떻게 테스트할 것인가?
@SpringBatchTest가 자동 등록하는 StepScopeTestExecutionListener가 이 문제를 해결한다StepExecution을 자동 생성StepSynchronizationManager.register(stepExecution)으로 StepScope 활성화StepSynchronizationManager.close()로 정리StepSynchronizationManager)을 사용하므로, @StepScope 빈을 @Autowired로 주입받아 테스트할 수 있게 된다JobScopeTestExecutionListener도 동일한 원리로 @JobScope 빈을 지원한다getStepExecution() 메서드를 정의하면, StepScopeTestExecutionListener가 리플렉션으로 자동 탐지·호출하여 커스텀 StepExecution을 사용한다
getStepExecution)과 반환 타입(StepExecution)만 맞추면 된다StepExecution이 자동 생성된다JobParameters는 JobExecution → StepExecution 체인으로 연결되므로, 커스텀 StepExecution의 부모 JobExecution에 원하는 JobParameters를 설정하면 @Value("#{jobParameters['key']}")로 주입받을 수 있다getJobExecution()을 정의하면 JobScopeTestExecutionListener가 탐지한다StepExecution 생성 시 MetaDataInstanceFactory를 사용하면 JobRepository 의존 없이 한 줄로 생성할 수 있다// 메서드 이름과 반환 타입만 맞추면 StepScopeTestExecutionListener가 자동 탐지
public StepExecution getStepExecution() {
JobParameters params = new JobParametersBuilder()
.addString("filePath", "/test/path")
.addLong("random", new SecureRandom().nextLong())
.toJobParameters();
return MetaDataInstanceFactory.createStepExecution(params);
}
getStepExecution()은 테스트 인스턴스 초기화 단계에서 호출되므로 두 가지 제약이 있다:
JobParameters를 설정할 수 없다 — 모든 테스트 메서드가 동일한 StepExecution을 공유하게 된다@TempDir 등 JUnit 필드가 아직 초기화되지 않은 시점이므로 접근할 수 없다 — 별도의 Path 필드를 만들어 Files.createTempDirectory()로 우회해야 하는 불편함getStepExecution()의 한계를 해결하는 유틸리티
StepExecution을 생성하고 StepScope를 활성화할 수 있다doInStepScope(stepExecution, callable)
StepExecution으로 StepScope를 활성화한 뒤 callable을 실행한다StepSynchronizationManager.register() → callable.call() → close()를 수행하므로, StepScopeTestExecutionListener와 동일한 메커니즘@JobScope 빈 테스트에는 JobScopeTestUtils.doInJobScope()를 동일한 방식으로 사용// 테스트 메서드 내에서 직접 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 인터페이스의 open()/close()를 직접 호출해야 한다FlatFileItemWriter 등은 open()에서 파일 리소스 초기화, close()에서 해제를 수행하므로 생략하면 테스트가 실패한다