348 lines
12 KiB
Plaintext
348 lines
12 KiB
Plaintext
|
|
[[testing]]
|
|
= Unit Testing
|
|
|
|
As with other application styles, it is extremely important to unit test any code written
|
|
as part of a batch job. The Spring core documentation covers how to unit and integration
|
|
test with Spring in great detail, so it is not be repeated here. It is important, however,
|
|
to think about how to "`end to end`" test a batch job, which is what this chapter covers.
|
|
The `spring-batch-test` project includes classes that facilitate this end-to-end test
|
|
approach.
|
|
|
|
[[creatingUnitTestClass]]
|
|
== Creating a Unit Test Class
|
|
|
|
For the unit test to run a batch job, the framework must load the job's
|
|
`ApplicationContext`. Two annotations are used to trigger this behavior:
|
|
|
|
* `@SpringJUnitConfig` indicates that the class should use Spring's
|
|
JUnit facilities
|
|
* `@SpringBatchTest` injects Spring Batch test utilities (such as the
|
|
`JobLauncherTestUtils` and `JobRepositoryTestUtils`) in the test context
|
|
|
|
NOTE: If the test context contains a single `Job` bean definition, this
|
|
bean will be autowired in `JobLauncherTestUtils`. Otherwise, the job
|
|
under test should be manually set on the `JobLauncherTestUtils`.
|
|
|
|
|
|
[tabs]
|
|
====
|
|
Java::
|
|
+
|
|
The following Java example shows the annotations in use:
|
|
+
|
|
.Using Java Configuration
|
|
[source, java]
|
|
----
|
|
@SpringBatchTest
|
|
@SpringJUnitConfig(SkipSampleConfiguration.class)
|
|
public class SkipSampleFunctionalTests { ... }
|
|
----
|
|
|
|
XML::
|
|
+
|
|
The following XML example shows the annotations in use:
|
|
+
|
|
.Using XML Configuration
|
|
[source, java]
|
|
----
|
|
@SpringBatchTest
|
|
@SpringJUnitConfig(locations = { "/simple-job-launcher-context.xml",
|
|
"/jobs/skipSampleJob.xml" })
|
|
public class SkipSampleFunctionalTests { ... }
|
|
----
|
|
|
|
====
|
|
|
|
|
|
|
|
|
|
[[endToEndTesting]]
|
|
== End-To-End Testing of Batch Jobs
|
|
|
|
"`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.
|
|
|
|
Consider an example of a batch job that 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` by using the
|
|
`launchJob()` method. The `launchJob()` method is provided by the `JobLauncherTestUtils`
|
|
class. The `JobLauncherTestUtils` class also provides the `launchJob(JobParameters)`
|
|
method, which lets the test give particular parameters. The `launchJob()` method
|
|
returns the `JobExecution` object, which is useful for asserting particular information
|
|
about the `Job` run. In the following case, the test verifies that the `Job` ended with
|
|
a status of `COMPLETED`.
|
|
|
|
|
|
[tabs]
|
|
====
|
|
Java::
|
|
+
|
|
The following listing shows an example with JUnit 5 in Java configuration style:
|
|
+
|
|
.Java Based Configuration
|
|
[source, java]
|
|
----
|
|
@SpringBatchTest
|
|
@SpringJUnitConfig(SkipSampleConfiguration.class)
|
|
public class SkipSampleFunctionalTests {
|
|
|
|
@Autowired
|
|
private JobLauncherTestUtils jobLauncherTestUtils;
|
|
|
|
private JdbcTemplate jdbcTemplate;
|
|
|
|
@Autowired
|
|
public void setDataSource(DataSource dataSource) {
|
|
this.jdbcTemplate = new JdbcTemplate(dataSource);
|
|
}
|
|
|
|
@Test
|
|
public void testJob(@Autowired Job job) throws Exception {
|
|
this.jobLauncherTestUtils.setJob(job);
|
|
this.jdbcTemplate.update("delete from CUSTOMER");
|
|
for (int i = 1; i <= 10; i++) {
|
|
this.jdbcTemplate.update("insert into CUSTOMER values (?, 0, ?, 100000)",
|
|
i, "customer" + i);
|
|
}
|
|
|
|
JobExecution jobExecution = jobLauncherTestUtils.launchJob();
|
|
|
|
|
|
Assert.assertEquals("COMPLETED", jobExecution.getExitStatus().getExitCode());
|
|
}
|
|
}
|
|
----
|
|
|
|
|
|
XML::
|
|
+
|
|
The following listing shows an example with JUnit 5 in XML configuration style:
|
|
+
|
|
.XML Based Configuration
|
|
[source, java]
|
|
----
|
|
@SpringBatchTest
|
|
@SpringJUnitConfig(locations = { "/simple-job-launcher-context.xml",
|
|
"/jobs/skipSampleJob.xml" })
|
|
public class SkipSampleFunctionalTests {
|
|
|
|
@Autowired
|
|
private JobLauncherTestUtils jobLauncherTestUtils;
|
|
|
|
private JdbcTemplate jdbcTemplate;
|
|
|
|
@Autowired
|
|
public void setDataSource(DataSource dataSource) {
|
|
this.jdbcTemplate = new JdbcTemplate(dataSource);
|
|
}
|
|
|
|
@Test
|
|
public void testJob(@Autowired Job job) throws Exception {
|
|
this.jobLauncherTestUtils.setJob(job);
|
|
this.jdbcTemplate.update("delete from CUSTOMER");
|
|
for (int i = 1; i <= 10; i++) {
|
|
this.jdbcTemplate.update("insert into CUSTOMER values (?, 0, ?, 100000)",
|
|
i, "customer" + i);
|
|
}
|
|
|
|
JobExecution jobExecution = jobLauncherTestUtils.launchJob();
|
|
|
|
|
|
Assert.assertEquals("COMPLETED", jobExecution.getExitStatus().getExitCode());
|
|
}
|
|
}
|
|
----
|
|
====
|
|
|
|
|
|
[[testingIndividualSteps]]
|
|
== Testing Individual Steps
|
|
|
|
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 `JobLauncherTestUtils` class contains a method called `launchStep`,
|
|
which takes a step name and runs just that particular `Step`. This approach allows for
|
|
more targeted tests letting the test set up data for only that step and to validate its
|
|
results directly. The following example shows how to use the `launchStep` method to load a
|
|
`Step` by name:
|
|
|
|
[source, java]
|
|
----
|
|
JobExecution jobExecution = jobLauncherTestUtils.launchStep("loadFileStep");
|
|
----
|
|
|
|
|
|
|
|
[[testing-step-scoped-components]]
|
|
== Testing Step-Scoped Components
|
|
|
|
Often, the components that are configured for your steps at runtime use step scope and
|
|
late binding to inject context from the step or job execution. These are tricky to test as
|
|
standalone components, unless you have a way to set the context as if they were in a step
|
|
execution. That is the goal of two components in Spring Batch:
|
|
`StepScopeTestExecutionListener` and `StepScopeTestUtils`.
|
|
|
|
The listener is declared at the class level, and its job is to create a step execution
|
|
context for each test method, as the following example shows:
|
|
|
|
[source, java]
|
|
----
|
|
@SpringJUnitConfig
|
|
@TestExecutionListeners( { DependencyInjectionTestExecutionListener.class,
|
|
StepScopeTestExecutionListener.class })
|
|
public class StepScopeTestExecutionListenerIntegrationTests {
|
|
|
|
// This component is defined step-scoped, so it cannot be injected unless
|
|
// a step is active...
|
|
@Autowired
|
|
private ItemReader<String> reader;
|
|
|
|
public StepExecution getStepExecution() {
|
|
StepExecution execution = MetaDataInstanceFactory.createStepExecution();
|
|
execution.getExecutionContext().putString("input.data", "foo,bar,spam");
|
|
return execution;
|
|
}
|
|
|
|
@Test
|
|
public void testReader() {
|
|
// The reader is initialized and bound to the input data
|
|
assertNotNull(reader.read());
|
|
}
|
|
|
|
}
|
|
----
|
|
|
|
There are two `TestExecutionListeners`. One is the regular Spring Test framework, which
|
|
handles dependency injection from the configured application context to inject the reader.
|
|
The other is the Spring Batch `StepScopeTestExecutionListener`. It works by looking for a
|
|
factory method in the test case for a `StepExecution`, using that as the context for the
|
|
test method, as if that execution were active in a `Step` at runtime. The factory method
|
|
is detected by its signature (it must return a `StepExecution`). If a factory method is
|
|
not provided, a default `StepExecution` is created.
|
|
|
|
Starting from v4.1, the `StepScopeTestExecutionListener` and
|
|
`JobScopeTestExecutionListener` are imported as test execution listeners
|
|
if the test class is annotated with `@SpringBatchTest`. The preceding test
|
|
example can be configured as follows:
|
|
|
|
[source, java]
|
|
----
|
|
@SpringBatchTest
|
|
@SpringJUnitConfig
|
|
public class StepScopeTestExecutionListenerIntegrationTests {
|
|
|
|
// This component is defined step-scoped, so it cannot be injected unless
|
|
// a step is active...
|
|
@Autowired
|
|
private ItemReader<String> reader;
|
|
|
|
public StepExecution getStepExecution() {
|
|
StepExecution execution = MetaDataInstanceFactory.createStepExecution();
|
|
execution.getExecutionContext().putString("input.data", "foo,bar,spam");
|
|
return execution;
|
|
}
|
|
|
|
@Test
|
|
public void testReader() {
|
|
// The reader is initialized and bound to the input data
|
|
assertNotNull(reader.read());
|
|
}
|
|
|
|
}
|
|
----
|
|
|
|
The listener approach is convenient if you want the duration of the step scope to be the
|
|
execution of the test method. For a more flexible but more invasive approach, you can use
|
|
the `StepScopeTestUtils`. The following example counts the number of items available in
|
|
the reader shown in the previous example:
|
|
|
|
[source, java]
|
|
----
|
|
int count = StepScopeTestUtils.doInStepScope(stepExecution,
|
|
new Callable<Integer>() {
|
|
public Integer call() throws Exception {
|
|
|
|
int count = 0;
|
|
|
|
while (reader.read() != null) {
|
|
count++;
|
|
}
|
|
return count;
|
|
}
|
|
});
|
|
----
|
|
|
|
[[mockingDomainObjects]]
|
|
== Mocking Domain Objects
|
|
|
|
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
|
|
the following code snippet shows:
|
|
|
|
[source, java]
|
|
----
|
|
public class NoWorkFoundStepExecutionListener implements StepExecutionListener {
|
|
|
|
public ExitStatus afterStep(StepExecution stepExecution) {
|
|
if (stepExecution.getReadCount() == 0) {
|
|
return ExitStatus.FAILED;
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
----
|
|
|
|
The framework provides the preceding listener example 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 you may encounter when
|
|
you try to unit test classes that implement interfaces requiring Spring Batch domain
|
|
objects. Consider the following unit test for the listener's in the preceding example:
|
|
|
|
[source, java]
|
|
----
|
|
private NoWorkFoundStepExecutionListener tested = new NoWorkFoundStepExecutionListener();
|
|
|
|
@Test
|
|
public void noWork() {
|
|
StepExecution stepExecution = new StepExecution("NoProcessingStep",
|
|
new JobExecution(new JobInstance(1L, new JobParameters(),
|
|
"NoProcessingJob")));
|
|
|
|
stepExecution.setExitStatus(ExitStatus.COMPLETED);
|
|
stepExecution.setReadCount(0);
|
|
|
|
ExitStatus exitStatus = tested.afterStep(stepExecution);
|
|
assertEquals(ExitStatus.FAILED.getExitCode(), exitStatus.getExitCode());
|
|
}
|
|
----
|
|
|
|
Because the Spring Batch domain model follows good object-oriented principles, the
|
|
`StepExecution` requires a `JobExecution`, which requires a `JobInstance` and
|
|
`JobParameters`, 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, as the following example shows:
|
|
|
|
[source, java]
|
|
----
|
|
private NoWorkFoundStepExecutionListener tested = new NoWorkFoundStepExecutionListener();
|
|
|
|
@Test
|
|
public void testAfterStep() {
|
|
StepExecution stepExecution = MetaDataInstanceFactory.createStepExecution();
|
|
|
|
stepExecution.setExitStatus(ExitStatus.COMPLETED);
|
|
stepExecution.setReadCount(0);
|
|
|
|
ExitStatus exitStatus = tested.afterStep(stepExecution);
|
|
assertEquals(ExitStatus.FAILED.getExitCode(), exitStatus.getExitCode());
|
|
}
|
|
----
|
|
|
|
The preceding method for creating a simple `StepExecution` is only one convenience method
|
|
available within the factory. You can find a full method listing in its
|
|
link:$$http://docs.spring.io/spring-batch/apidocs/org/springframework/batch/test/MetaDataInstanceFactory.html$$[Javadoc].
|