Skip to content

Issue with JobOperatorTestUtils.startJob() in Spring Batch 6.0.x. #5216

@manishsharma1992

Description

@manishsharma1992

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

  1. Create a job with a step containing a @StepScope reader 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();
    }
}
  1. 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());
    }
}
  1. Run the tests:
    • testWithStartJob()FAILS - filePath is null in the reader
    • testWithLaunchJob()PASSES - filePath is 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 @StepScope or potentially @JobScope beans with parameter injection
  • Interestingly, JobOperatorTestUtils.startStep() works correctly and properly passes parameters to @StepScope beans

This suggests the issue is in how startJob() internally creates or propagates the execution context to steps, while launchJob() and startStep() handle it correctly.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions