-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Description
Bug description
JobOperatorTestUtils.startJob(JobParameters) fails to propagate job parameters to @StepScope beans, causing @Value("#{jobParameters['paramName']}") to resolve as null. This makes it impossible to test jobs with step-scoped beans that depend on job parameters when using this method.
However, JobOperatorTestUtils.launchJob(String jobName, Properties properties) works correctly and properly propagates parameters to @StepScope beans.
Environment
- Spring Boot version: 4.0.1
- Spring Batch version: 6.0.1 (via
spring-boot-starter-batch) - Java version: 17+
- Database: PostgreSQL with H2 for tests
- Build tool: Maven/Gradle
- Testing framework: JUnit 5 with
@SpringBatchTest
Steps to reproduce
- Create a job with a step containing a
@StepScopereader that uses job parameters:
@Configuration
public class BatchConfig {
@Bean
@StepScope
public FlatFileItemReader<String> reader(
@Value("#{jobParameters['filePath']}") String filePath) {
System.out.println("FilePath received: " + filePath); // Prints null with startJob()
if (filePath == null) {
throw new IllegalArgumentException("filePath is null");
}
return new FlatFileItemReaderBuilder<String>()
.name("reader")
.resource(new FileSystemResource(filePath))
.lineMapper((line, lineNumber) -> line)
.build();
}
@Bean
public Step importStep(JobRepository jobRepository,
PlatformTransactionManager transactionManager,
FlatFileItemReader<String> reader) {
return new StepBuilder("importStep", jobRepository)
.<String, String>chunk(10, transactionManager)
.reader(reader)
.writer(items -> System.out.println("Processing: " + items))
.build();
}
@Bean
public Job testJob(JobRepository jobRepository, Step importStep) {
return new JobBuilder("testJob", jobRepository)
.start(importStep)
.build();
}
}- Write a test using
JobOperatorTestUtils.startJob():
@SpringBatchTest
@SpringBootTest
@TestPropertySource(properties = {"spring.batch.job.enabled=false"})
public class JobParameterBugTest {
@Autowired
private JobOperatorTestUtils jobOperatorTestUtils;
@Autowired
private Job testJob;
@Test
public void testWithStartJob() throws Exception {
jobOperatorTestUtils.setJob(testJob);
JobParameters jobParameters = new JobParametersBuilder()
.addString("filePath", "/tmp/test-file.txt")
.addLong("timestamp", System.currentTimeMillis())
.toJobParameters();
// This fails - filePath is null in @StepScope bean
JobExecution execution = jobOperatorTestUtils.startJob(jobParameters);
// Job fails with "filePath is null" error
}
@Test
public void testWithLaunchJob() throws Exception {
Properties jobProperties = new Properties();
jobProperties.setProperty("filePath", "/tmp/test-file.txt");
jobProperties.setProperty("timestamp", String.valueOf(System.currentTimeMillis()));
// This works - filePath is correctly received
JobExecution execution = jobOperatorTestUtils.launchJob(
testJob.getName(),
jobProperties
);
assertEquals(BatchStatus.COMPLETED, execution.getStatus());
}
}- Run the tests:
testWithStartJob()→ FAILS -filePathisnullin the readertestWithLaunchJob()→ PASSES -filePathis correctly received
Expected behavior
JobOperatorTestUtils.startJob(JobParameters) should propagate job parameters to the step execution context so that @StepScope beans can resolve @Value("#{jobParameters['paramName']}") expressions correctly, just like launchJob() does.
Minimal Complete Reproducible example
// pom.xml dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-batch</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>org.springframework.batch</groupId>
<artifactId>spring-batch-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
// BatchConfiguration.java
@Configuration
@EnableBatchProcessing
public class BatchConfiguration {
@Bean
@StepScope
public FlatFileItemReader<String> fileReader(
@Value("#{jobParameters['inputFile']}") String inputFile) {
if (inputFile == null) {
throw new IllegalArgumentException("inputFile parameter is null!");
}
return new FlatFileItemReaderBuilder<String>()
.name("fileReader")
.resource(new ClassPathResource(inputFile))
.lineMapper((line, lineNumber) -> line)
.build();
}
@Bean
public Step processStep(JobRepository jobRepository,
PlatformTransactionManager transactionManager,
FlatFileItemReader<String> fileReader) {
return new StepBuilder("processStep", jobRepository)
.<String, String>chunk(10, transactionManager)
.reader(fileReader)
.writer(items -> System.out.println("Items: " + items))
.build();
}
@Bean
public Job fileProcessingJob(JobRepository jobRepository, Step processStep) {
return new JobBuilder("fileProcessingJob", jobRepository)
.start(processStep)
.build();
}
}
// JobOperatorTestUtilsBugTest.java
@SpringBatchTest
@SpringBootTest
@TestPropertySource(properties = {"spring.batch.job.enabled=false"})
public class JobOperatorTestUtilsBugTest {
@Autowired
private JobOperatorTestUtils jobOperatorTestUtils;
@TempDir
Path tempDir;
@BeforeEach
void createTestFile() throws IOException {
Path testFile = tempDir.resolve("test-data.txt");
Files.write(testFile, List.of("line1", "line2", "line3"));
}
@Test
public void testStartJob_shouldPropagateParameters(@Autowired Job fileProcessingJob) throws Exception {
jobOperatorTestUtils.setJob(fileProcessingJob);
JobParameters params = new JobParametersBuilder()
.addString("inputFile", tempDir.resolve("test-data.txt").toString())
.addLong("timestamp", System.currentTimeMillis())
.toJobParameters();
// BUG: This throws "inputFile parameter is null!"
JobExecution execution = jobOperatorTestUtils.startJob(params);
// Expected: Job completes successfully with parameters propagated
assertEquals(BatchStatus.COMPLETED, execution.getStatus());
}
@Test
public void testLaunchJob_worksCorrectly(@Autowired Job fileProcessingJob) throws Exception {
Properties props = new Properties();
props.setProperty("inputFile", tempDir.resolve("test-data.txt").toString());
props.setProperty("timestamp", String.valueOf(System.currentTimeMillis()));
// WORKAROUND: This works correctly
JobExecution execution = jobOperatorTestUtils.launchJob(
fileProcessingJob.getName(),
props
);
assertEquals(BatchStatus.COMPLETED, execution.getStatus());
}
}Additional context
- The issue appears to be specific to
JobOperatorTestUtils.startJob(JobParameters)method - Using
JobOperatorTestUtils.launchJob(String, Properties)as a workaround works correctly - Using
JobLauncherTestUtils.launchJob(JobParameters)also works correctly - The bug affects jobs that use
@StepScopeor potentially@JobScopebeans with parameter injection - Interestingly,
JobOperatorTestUtils.startStep()works correctly and properly passes parameters to@StepScopebeans
This suggests the issue is in how startJob() internally creates or propagates the execution context to steps, while launchJob() and startStep() handle it correctly.