Just as with other application styles, it is extremely important to unit test any code written as part of a batch job as well. The Spring core documentation covers how to unit and integration test with Spring in great detail, so it won't be repeated here. It is important, however, to think about how to 'end to end' test a batch job, which is what this chapter will focus on. The spring-batch-test project includes classes that will help facilitate this end-to-end test approach.
In order for the unit test to run a batch job, the framework must load the job's ApplicationContext. Two annotations are used to trigger this:
@RunWith(SpringJUnit4ClassRunner.class)
:
Indicates that the class should use Spring's JUnit facilities
@ContextConfiguration(locations = {...})
:
Indicates which XML files contain the ApplicationContext.
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = { "/simple-job-launcher-context.xml", "/jobs/skipSampleJob.xml" }) public class SkipSampleFunctionalTests extends AbstractJobTests { ... }
'End To End' testing can be defined as testing the complete run of a batch job from beginning to end. This allows for a test that sets up a test condition, executes the job, and verifies the end result.
In the example below, the batch job reads from the database and
writes to a flat file. The test method begins by setting up the database
with test data. It clears the CUSTOMER table and then inserts 10 new
records. The test then launches the Job
using the
launchJob()
method. The
launchJob
() method is provided by the
AbstractJobTests
parent class. Also provided by the
super class is launchJob(JobParameters)
, which
allows the test to give particular parameters. The
launchJob()
method returns the
JobExecution
object which is useful for asserting
particular information about the Job
run. In the
case below, the test verifies that the Job
ended
with status "COMPLETED".
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "/simple-job-launcher-context.xml",
"/jobs/skipSampleJob.xml" })
public class SkipSampleFunctionalTests extends AbstractJobTests {
private SimpleJdbcTemplate simpleJdbcTemplate;
@Autowired
public void setDataSource(DataSource dataSource) {
this.simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource);
}
@Transactional
@Test
public void testJob() throws Exception {
simpleJdbcTemplate.update("delete from CUSTOMER");
for (int i = 1; i <= 10; i++) {
simpleJdbcTemplate.update("insert into CUSTOMER values (?, 0, ?, 100000)",
i, "customer" + i);
}
JobExecution jobExecution = this.launchJob();
Assert.assertEquals("COMPLETED", jobExecution.getExitStatus());
}
}
For complex batch jobs, test cases in the end-to-end testing
approach may become unmanageable. It these cases, it may be more useful to
have test cases to test individual steps on their own. The
AbstractJobTests
class contains a method
launchStep
that takes a step name and runs just
that particular Step
. This approach allows for more
targeted tests by allowing the test to set up data for just that step and
to validate its results directly.
JobExecution jobExecution = this.launchStep("loadFileStep");
When a batch job writes to the database, it is easy to query the
database to verify that the output is as expected. However, if the batch
job writes to a file, it is equally important that the output be verified.
Spring Batch provides a class AssertFile
to
facilitate the verification of output files. The method
assertFileEquals
takes two
File
objects (or two
Resource
objects) and asserts, line by line, that
the two files have the same content. Therefore, it is possible to create a
file with the expected output and to compare it to the actual
result:
private static final String EXPECTED_FILE = "src/main/resources/data/input.txt"; private static final String OUTPUT_FILE = "target/test-outputs/output.txt"; AssertFile.assertFileEquals(new FileSystemResource(EXPECTED_FILE), new FileSystemResource(OUTPUT_FILE));
Another common issue encountered while writing unit and integration
tests for Spring Batch components is how to mock domain objects. A good
example is a StepExecutionListener
, as illustrated
below:
public class NoWorkFoundStepExecutionListener extends StepExecutionListenerSupport { public ExitStatus afterStep(StepExecution stepExecution) { if (stepExecution.getReadCount() == 0) { throw new NoWorkFoundException("Step has not processed any items"); } return stepExecution.getExitStatus(); } }
The above listener is provided by the framework and checks a
StepExecution
for an empty read count, thus
signifying that no work was done. While this example is fairly simple, it
serves to illustrate the types of problems that may be encountered when
attempting to unit test classes that implement interfaces requiring Spring
Batch domain objects. Consider the above listener's unit test:
private NoWorkFoundStepExecutionListener tested = new NoWorkFoundStepExecutionListener();
@Test
public void testAfterStep() {
StepExecution stepExecution = new StepExecution("NoProcessingStep",
new JobExecution(new JobInstance(1L, new JobParameters(),
"NoProcessingJob")));
stepExecution.setReadCount(0);
try {
tested.afterStep(stepExecution);
fail();
} catch (NoWorkFoundException e) {
assertEquals("Step has not processed any items", e.getMessage());
}
}
Because the Spring Batch domain model follows good object orientated
principles, the StepExecution requires a
JobExecution
, which requires a
JobInstance
and
JobParameters
in order to create a valid
StepExecution
. While this is good in a solid domain
model, it does make creating stub objects for unit testing verbose. To
address this issue, the Spring Batch test module includes a factory for
creating domain objects: MetaDataInstanceFactory
.
Given this factory, the unit test can be updated to be more
concise:
private NoWorkFoundStepExecutionListener tested = new NoWorkFoundStepExecutionListener();
@Test
public void testAfterStep() {
StepExecution stepExecution = MetaDataInstanceFactory.createStepExecution();
stepExecution.setReadCount(0);
try {
tested.afterStep(stepExecution);
fail();
} catch (NoWorkFoundException e) {
assertEquals("Step has not processed any items", e.getMessage());
}
}
The above method for creating a simple
StepExecution
is just one convenience method
available within the factory. A full method listing can be found in its
Javadoc.