Skip to content

Enhance ResourcelessJobRepository implementation for testing #5139

@benelog

Description

@benelog

Background

I understand the design intent behind ResourcelessJobRepository, based on the following issue:

However, I believe there are a few targeted enhancements that would make this class much more useful in tests, without violating the original design goals.

In a single application context, ResourcelessJobRepository cannot run the same job multiple times. This limitation is acceptable as long as users understand it, but in tests it can become a constraint.

For example, the following test runs the same job with different JobParameters using JobOperatorTestUtils, but cannot rely on ResourcelessJobRepository:

@SpringBootTest("spring.batch.job.enabled=false")
@SpringBatchTest
class HelloParamJobTest {

  @Autowired
  JobOperatorTestUtils testUtils;

  @BeforeEach
  void prepareTestUtils(@Autowired @Qualifier("helloParamJob") Job helloParamJob) {
    testUtils.setJob(helloParamJob);
  }

  @Test
  void startJob() throws Exception {
    JobParameters params = testUtils.getUniqueJobParametersBuilder()
        .addLocalDate("helloDate", LocalDate.of(2025, 7, 28))
        .toJobParameters();
    JobExecution execution = testUtils.startJob(params);
    assertThat(execution.getExitStatus()).isEqualTo(ExitStatus.COMPLETED);
  }

  @Test
  void startJobWithInvalidJobParameters() {
    JobParameters params = testUtils.getUniqueJobParametersBuilder()
        .addLocalDate("goodDate", LocalDate.of(2025, 7, 28))
        .toJobParameters();
    assertThatExceptionOfType(InvalidJobParametersException.class)
        .isThrownBy(() -> testUtils.startJob(params))
        .withMessageContaining("do not contain required keys: [helloDate]");
  }
}

Known solutions

I am aware of several existing workarounds for this limitation:

  • Register ResourcelessJobRepository as a prototype-scoped bean.
  • Use an in-memory database together with a JDBC-based JobRepository.
  • Use @DirtiesContext (or similar) to refresh the ApplicationContext before each test.

However, all of these come with trade-offs, such as:

  • Additional complexity in object wiring and dependencies.
  • Extra configuration overhead.
  • Slower test execution due to frequent context refreshes.

In particular, as mentioned in this comment:

calls to ResourcelessJobRepository#getJobInstance(String, JobParameters) have caused test scenarios that worked well with  Spring Batch v5.2 to become impossible when upgrading to v6.0.

In such cases, users may see an error like:

Message: A job instance already exists and is complete for identifying parameters={JobParameter{name='batch.random', value=4546055881725385948}

This can be confusing because the job name and/or JobParameters are actually different, yet the repository still resolves them to the same JobInstance. This also feels misaligned with Spring Batch's conceptual model, where a JobInstance is uniquely identified by a job name and its JobParameters.

Proposed enhancements

To address these issues while preserving the original design, I would like to propose the following enhancements to ResourcelessJobRepository:

1. Filter return values based on job name, IDs, and parameters

For the methods below, compare the incoming arguments (jobName, instanceId, executionId, JobParameters, etc.) with the values held in the internal JobInstance and JobExecution fields, and filter return values accordingly:

  • getJobInstances(String jobName, int start, int count)
  • findJobInstances(String jobName)
  • getJobInstance(long instanceId)
  • getLastJobInstance(String jobName)
  • getJobInstance(String jobName, JobParameters jobParameters)
  • getJobInstanceCount(String jobName)
  • getJobExecution(long executionId)
  • getLastJobExecution(String jobName, JobParameters jobParameters)
  • getLastJobExecution(JobInstance jobInstance)
  • getJobExecutions(JobInstance jobInstance)

Some methods already have comments like // FIXME should return null if the id is not matching, which suggest that this kind of filtering was already considered. Even if only a subset of these methods were updated, the observable behavior might be acceptable in practice. However, for conceptual consistency and to future-proof the implementation, I believe it would be better to have a systematic comparison of jobName, jobInstanceId, etc., across the class.

2. Add methods to delete the current JobInstance and JobExecution

Add the ability to drop the currently held JobInstance and JobExecution from ResourcelessJobRepository:

  • deleteJobInstance(JobInstance jobInstance)
  • deleteJobExecution(JobExecution jobExecution)

If these methods are introduced, tests could intentionally delete the just-run JobInstance or JobExecution to reuse the same ResourcelessJobRepository instance in a more flexible way. For example:

@Autowired
JobOperatorTestUtils testUtils;

@Autowired
JobRepository repository;

@BeforeEach
void prepareTestUtils(@Autowired @Qualifier("helloParamJob") Job helloParamJob) {
  testUtils.setJob(helloParamJob);
}

@Test
void startJob() throws Exception {
  JobParameters params = testUtils.getUniqueJobParametersBuilder()
      .addLocalDate("helloDate", LocalDate.of(2025, 7, 28))
      .toJobParameters();
  JobExecution execution = testUtils.startJob(params);
  assertThat(execution.getExitStatus()).isEqualTo(ExitStatus.COMPLETED);

  // Explicitly clear the current JobInstance for the next test
  repository.deleteJobInstance(execution.getJobInstance());
}

These two enhancements together would:

  • Make the implementation more faithful to the JobRepository interface contract.
    • Reduce surprising behavior where different jobs/parameters map to the same JobInstance.
  • Make ResourcelessJobRepository more useful in test scenarios, especially when upgrading from Spring Batch 5.x to 6.x.
  • Maintain the original in-memory, single-instance nature of ResourcelessJobRepository.

I would be happy to open a PR or further refine this proposal based on feedback from the maintainers.

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