Unit Testing
与其他应用风格一样,单元测试批处理作业的一部分编写的任何代码至关重要。Spring 核心文档详细介绍了如何使用 Spring 进行单元和集成测试,因此这里将不再赘述。但是,考虑如何“端到端”测试批处理作业非常重要,这也是本章涵盖的内容。spring-batch-test
项目包含有助于这种端到端测试方法的类。
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.
Creating a Unit Test Class
为了让单元测试运行批处理作业,框架必须加载作业的 ApplicationContext
。两个注释用于触发此行为:
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 theJobLauncherTestUtils
andJobRepositoryTestUtils
) in the test context
如果测试上下文包含单个 |
If the test context contains a single |
- Java
-
以下 Java 示例显示了正在使用的注释:
The following Java example shows the annotations in use:
@SpringBatchTest
@SpringJUnitConfig(SkipSampleConfiguration.class)
public class SkipSampleFunctionalTests { ... }
- XML
-
以下 XML 示例显示了正在使用的注释:
The following XML example shows the annotations in use:
@SpringBatchTest
@SpringJUnitConfig(locations = { "/simple-job-launcher-context.xml",
"/jobs/skipSampleJob.xml" })
public class SkipSampleFunctionalTests { ... }
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.
考虑读取数据库并写入平面文件的批处理作业的示例。测试方法从使用测试数据设置数据库开始。它将 CUSTOMER
表清除,然后插入 10 条新记录。然后测试通过使用 launchJob()
方法启动 Job
。JobLauncherTestUtils
类提供了 launchJob()
方法。JobLauncherTestUtils
类还提供了 launchJob(JobParameters)
方法,它允许测试给予特定参数。launchJob()
方法返回 JobExecution
对象,该对象对于断言有关 Job
运行的特定信息非常有用。在以下示例中,测试验证了 Job
以 COMPLETED
状态结束。
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
.
- Java
-
以下清单展示了使用 Java 配置风格的 JUnit 5 的示例:
The following listing shows an example with JUnit 5 in Java configuration style:
@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
-
以下清单展示了使用 XML 配置风格的 JUnit 5 的示例:
The following listing shows an example with JUnit 5 in XML configuration style:
@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());
}
}
Testing Individual Steps
对于复杂的批处理作业,端到端测试方法中的测试用例可能会变得难以管理。在这种情况下,拥有测试用例来单独测试各个步骤可能会更有用。JobLauncherTestUtils
类包含一个名为 launchStep
的方法,它采用步骤名称并仅运行该特定 Step
。这种方法允许进行更具针对性的测试,让测试仅为该步骤设置数据,并直接验证其结果。以下示例显示如何使用 launchStep
方法按名称加载 Step
:
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:
JobExecution jobExecution = jobLauncherTestUtils.launchStep("loadFileStep");
Testing Step-Scoped Components
通常,在运行时为步骤配置的组件使用步骤作用域和延迟绑定来从步骤或作业执行注入上下文。这些组件很难作为独立组件进行测试,除非你有办法为这些组件设置上下文(就好像它们在一个步骤执行中一样)。这是 Spring Batch 中两个组件的目标:StepScopeTestExecutionListener
和 StepScopeTestUtils
。
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:
@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());
}
}
有两个 TestExecutionListeners
。一个是常规的 Spring 测试框架,它处理从配置的应用程序上下文中注入依存关系以便注入读取器。另一个是 Spring Batch StepScopeTestExecutionListener
。它的工作原理是寻找一个 StepExecution
的测试用例中的工厂方法,并将其用作测试方法的上下文,就好像该执行在运行时在 Step
中处于活动状态一样。工厂方法是通过其签名检测到的(它必须返回一个 StepExecution
)。如果没有提供工厂方法,便会创建一个默认的 StepExecution
。
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.
从 v4.1 开始,如果测试类带有 @SpringBatchTest
注释,StepScopeTestExecutionListener
和 JobScopeTestExecutionListener
会被导入为测试执行侦听器。前面的测试示例可以配置为:
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:
@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());
}
}
如果您希望步骤作用域的持续时间为执行测试方法,侦听器方法很方便。对于更为灵活但更具侵入性的方法,您可以使用 StepScopeTestUtils
。以下示例会统计在前面示例中显示的读取器中可用的项目数量:
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:
int count = StepScopeTestUtils.doInStepScope(stepExecution,
new Callable<Integer>() {
public Integer call() throws Exception {
int count = 0;
while (reader.read() != null) {
count++;
}
return count;
}
});
Validating Output Files
当一个批处理作业写入数据库时,很容易查询数据库以验证输出是否符合预期。但是,如果批处理作业写入文件,则同样重要的是验证输出。Spring Batch 提供了一个名为 AssertFile
的类,以方便验证输出文件。名为 assertFileEquals
的方法会获取两个 File
对象(或两个 Resource
对象),并逐行断言两个文件具有相同的内容。因此,可以创建一个具有预期输出的文件并将其与实际结果进行比较,如下面的示例所示:
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 called AssertFile
to facilitate the verification of output files. The method called 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, as the following example shows:
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));
Mocking Domain Objects
在为 Spring Batch 组件编写单元和集成测试时遇到的另一个常见问题是如何模拟领域对象。一个好的示例是 StepExecutionListener
,如下面的代码段所示:
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:
public class NoWorkFoundStepExecutionListener extends StepExecutionListenerSupport {
public ExitStatus afterStep(StepExecution stepExecution) {
if (stepExecution.getReadCount() == 0) {
return ExitStatus.FAILED;
}
return null;
}
}
该框架提供了前面的侦听器示例,并针对空读计数检查 StepExecution
,从而表明没有完成任何工作。虽然这个示例非常简单,但它有助于说明在尝试对实现需要 Spring Batch 领域对象的接口的类进行单元测试时您可能会遇到的问题类型。考虑一下对前面示例中侦听器的单元测试:
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:
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());
}
由于 Spring Batch 领域模型遵循良好的面向对象原则,StepExecution
需要一个 JobExecution
,JobExecution
需要一个 JobInstance
和 JobParameters
,才能创建一个有效的 StepExecution
。虽然这在可靠的领域模型中很好,但它会使创建用于单元测试的存根对象变得冗长。为了解决这个问题,Spring Batch 测试模块中包含用于创建领域对象的工厂:MetaDataInstanceFactory
。指定了此工厂,就可以更新单元测试以使其更加简洁,如下面的示例所示:
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:
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());
}
用于创建简单 `StepExecution`的前述方法只是该工厂中可用的一个便捷方法。你可以在其 Javadoc中找到方法的完整列表。
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
Javadoc.