A lightweight, comprehensive batch framework designed to enable the development of robust batch applications vital for the daily operations of enterprise systems.
@Test
public void filterId() throws Exception {
final Customer customer = new Customer();
customer.setId(1);
customer.setName("name");
customer.setBirthday(new GregorianCalendar());
final int id = StepScopeTestUtils.doInStepScope(
getStepExecution(),
() -> processor.process(customer).getId()
);
Assert.assertEquals(1, id);
}
Another approach is based on the StepScopeTestUtils utility class. This class is used to create and manipulate StepScope in unit tests in a more flexible way without using dependency injection. For example, reading the ID of the customer filtered by the processor above could be done as follows:
There are two TestExecutionListeners. One is from the regular Spring Test framework and handles dependency injection from the configured application context. The other is the Spring Batch StepScopeTestExecutionListener that sets up step-scope context for dependency injection into unit tests. A StepContext is created for the duration of a test method and made available to any dependencies that are injected. The default behavior is just to create a StepExecution with fixed properties. Alternatively, the StepContext can be provided by the test case as a factory method returning the correct type.
@RunWith(SpringRunner.class)
@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, StepScopeTestExecutionListener.class})
@ContextConfiguration(classes = {BatchApplication.class, BatchTestConfiguration.class})
public class BirthdayFilterProcessorTest {
@Autowired
private BirthdayFilterProcessor processor;
public StepExecution getStepExecution() {
return MetaDataInstanceFactory.createStepExecution();
}
@Test
public void filter() throws Exception {
final Customer customer = new Customer();
customer.setId(1);
customer.setName("name");
customer.setBirthday(new GregorianCalendar());
Assert.assertNotNull(processor.process(customer));
}
}
The TestExecutionListeners are declared at the class level, and its job is to create a step execution context for each test method. For example:
Spring Batch introduces additional scopes for step and job contexts. Objects in these scopes use the Spring container as an object factory, so there is only one instance of each such bean per execution step or job. In addition, support is provided for late binding of references accessible from the StepContext or JobContext. The components that are configured at runtime to be step- or job-scoped are tricky to test as standalone components unless you have a way to set the context as if they were in a step or job execution. That is the goal of the org.springframework.batch.test.StepScopeTestExecutionListener and org.springframework.batch.test.StepScopeTestUtils components in Spring Batch, as well as JobScopeTestExecutionListener and JobScopeTestUtils.
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {BatchApplication.class, BatchTestConfiguration.class})
public class CustomerReportJobConfigTest {
@Autowired
private JobLauncherTestUtils testUtils;
@Autowired
private CustomerReportJobConfig config;
@Test
public void testEntireJob() throws Exception {
final JobExecution result = testUtils.getJobLauncher().run(config.customerReportJob(), testUtils.getUniqueJobParameters());
Assert.assertNotNull(result);
Assert.assertEquals(BatchStatus.COMPLETED, result.getStatus());
}
@Test
public void testSpecificStep() {
Assert.assertEquals(BatchStatus.COMPLETED, testUtils.launchStep("taskletStep").getStatus());
}
}
A typical test for a job and a step looks as follows (and can use any mocking frameworks as well):
@Configuration
public class BatchTestConfiguration {
@Bean
public JobLauncherTestUtils jobLauncherTestUtils() {
return new JobLauncherTestUtils();
}
}
There is a utility class org.springframework.batch.test.JobLauncherTestUtils to test batch jobs. It provides methods for launching an entire job as well as allowing for end-to-end testing of individual steps without having to run every step in the job. It must be declared as a Spring bean:
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {...})
Usually, to run unit tests in a Spring Boot application, the framework must load a corresponding ApplicationContext. Two annotations are used for this purpose:
Spring Batch Unit Testing
@Autowired
private JobOperator operator;
@Autowired
private JobExplorer jobs;
@Scheduled(fixedRate = 5000)
public void run() throws Exception {
List<JobInstance> lastInstances = jobs.getJobInstances(JOB_NAME, 0, 1);
if (lastInstances.isEmpty()) {
jobLauncher.run(customerReportJob(), new JobParameters());
} else {
operator.startNextInstance(JOB_NAME);
}
}
Alternatively, you can launch the next job in a sequence of JobInstances determined by the JobParametersIncrementer attached to the specified job with SimpleJobOperator.startNextInstance():
@Scheduled(fixedRate = 5000)
public void run() throws Exception {
jobLauncher.run(
customerReportJob(),
new JobParametersBuilder().addLong("uniqueness", System.nanoTime()).toJobParameters()
);
}
There are two ways of avoiding this problem when you schedule a batch job.
One is to be sure to introduce one or more unique parameters (e.g., actual start time in nanoseconds) to each job:
This happens because only unique JobInstances may be created and executed and Spring Batch has no way of distinguishing between the first and second JobInstance.
INFO 36988 --- [pool-2-thread-1] o.s.b.c.l.support.SimpleJobLauncher : Job: [SimpleJob: [name=customerReportJob]] launched with the following parameters: [{}]
INFO 36988 --- [pool-2-thread-1] o.s.batch.core.job.SimpleStepHandler : Step already complete or not restartable, so no action to execute: StepExecution: id=1, version=3, name=taskletStep, status=COMPLETED, exitStatus=COMPLETED, readCount=0, filterCount=0, writeCount=0 readSkipCount=0, writeSkipCount=0, processSkipCount=0, commitCount=1, rollbackCount=0, exitDescription=
INFO 36988 --- [pool-2-thread-1] o.s.batch.core.job.SimpleStepHandler : Step already complete or not restartable, so no action to execute: StepExecution: id=2, version=53, name=chunkStep, status=COMPLETED, exitStatus=COMPLETED, readCount=1000, filterCount=982, writeCount=18 readSkipCount=0, writeSkipCount=0, processSkipCount=0, commitCount=51, rollbackCount=0, exitDescription=
There is a problem with the above example though. At run time, the job will succeed the first time only. When it launches the second time (i.e. after five seconds), it will generate the following messages in the logs (note that in previous versions of Spring Batch a JobInstanceAlreadyCompleteException would have been thrown):