单元测试
与其他应用程序风格一样,对批处理作业中编写的任何代码进行单元测试极为重要。Spring核心文档详细介绍了如何使用Spring进行单元测试和集成测试,因此这里不再赘述。然而,思考如何对批处理作业进行“端到端”测试非常重要,这正是本章要讨论的内容。spring-batch-test项目包含了便于进行端到端测试的类。
创建单元测试类
在运行批处理作业的单元测试时,框架必须加载作业的 ApplicationContext。以下两个注解用于触发此行为:
-
@SpringJUnitConfig表示该类应使用 Spring 的 JUnit 功能 -
@SpringBatchTest在测试上下文中注入 Spring Batch 测试工具(例如JobOperatorTestUtils和JobRepositoryTestUtils)
如果测试上下文中包含单个 Job bean 定义,该 bean 将自动装配到 JobOperatorTestUtils 中。否则,需要手动在 JobOperatorTestUtils 上设置待测试的作业。
自 Spring Batch 6.0 起,不再支持 JUnit 4。建议迁移至 JUnit Jupiter。
- Java
- XML
以下 Java 示例展示了注解的使用:
@SpringBatchTest
@SpringJUnitConfig(SkipSampleConfiguration.class)
public class SkipSampleFunctionalTests { ... }
以下 XML 示例展示了注解的使用:
@SpringBatchTest
@SpringJUnitConfig(locations = { "/skip-sample-configuration.xml" })
public class SkipSampleFunctionalTests { ... }
批处理作业的端到端测试
“端到端”测试可以定义为测试批处理作业从开始到结束的完整运行过程。这种测试允许设置测试条件、执行作业并验证最终结果。
考虑一个从数据库读取数据并写入平面文件的批处理作业示例。测试方法首先使用测试数据设置数据库:清空 CUSTOMER 表,然后插入 10 条新记录。接着,测试通过 startJob() 方法启动 Job。startJob() 方法由 JobOperatorTestUtils 类提供。JobOperatorTestUtils 类还提供了 startJob(JobParameters) 方法,允许测试传入特定参数。startJob() 方法返回 JobExecution 对象,该对象可用于断言关于 Job 运行的特定信息。在以下示例中,测试验证了 Job 以 COMPLETED 状态结束。
- Java
- XML
以下清单展示了使用 JUnit 5 的 Java 配置风格示例:
@SpringBatchTest
@SpringJUnitConfig(SkipSampleConfiguration.class)
public class SkipSampleFunctionalTests {
@Autowired
private JobOperatorTestUtils jobOperatorTestUtils;
private JdbcTemplate jdbcTemplate;
@Autowired
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
@Test
public void testJob(@Autowired Job job) throws Exception {
this.jobOperatorTestUtils.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 = jobOperatorTestUtils.startJob();
Assert.assertEquals("COMPLETED", jobExecution.getExitStatus().getExitCode());
}
}
以下清单展示了使用 JUnit 5 的 XML 配置风格示例:
@SpringBatchTest
@SpringJUnitConfig(locations = { "/skip-sample-configuration.xml" })
public class SkipSampleFunctionalTests {
@Autowired
private JobOperatorTestUtils jobOperatorTestUtils;
private JdbcTemplate jdbcTemplate;
@Autowired
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
@Test
public void testJob(@Autowired Job job) throws Exception {
this.jobOperatorTestUtils.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 = jobOperatorTestUtils.startJob();
Assert.assertEquals("COMPLETED", jobExecution.getExitStatus().getExitCode());
}
}
测试独立步骤
对于复杂的批处理作业,端到端测试方法中的测试用例可能会变得难以管理。在这些情况下,为各个步骤单独编写测试用例可能更为实用。JobOperatorTestUtils 类包含一个名为 launchStep 的方法,该方法接收一个步骤名称并仅运行该特定 Step。这种方法允许进行更有针对性的测试,让测试仅为该步骤设置数据并直接验证其结果。以下示例展示了如何使用 startStep 方法按名称启动一个 Step:
JobExecution jobExecution = jobOperatorTestUtils.startStep("loadFileStep");
测试步骤作用域组件
通常,在运行时为步骤配置的组件会使用步骤作用域和延迟绑定来注入来自步骤或作业执行的上下文。除非有办法设置上下文,使其如同在步骤执行中一样,否则将这些组件作为独立组件进行测试会相当棘手。这正是Spring Batch中两个组件的目标:StepScopeTestExecutionListener 和 StepScopeTestUtils。
监听器在类级别声明,其任务是为每个测试方法创建步骤执行上下文,如下例所示:
@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());
}
}
有两个 TestExecutionListener。一个是常规的 Spring Test 框架,它负责从配置的应用上下文中处理依赖注入以注入 reader。另一个是 Spring Batch 的 StepScopeTestExecutionListener。它的工作方式是在测试用例中寻找一个用于创建 StepExecution 的工厂方法,并将其作为测试方法的上下文,就像该执行在运行时的 Step 中处于活动状态一样。该工厂方法通过其签名被检测到(它必须返回一个 StepExecution)。如果未提供工厂方法,则会创建一个默认的 StepExecution。
从 v4.1 版本开始,如果测试类被标注了 @SpringBatchTest,StepScopeTestExecutionListener 和 JobScopeTestExecutionListener 会被作为测试执行监听器自动导入。前面的测试示例可以配置如下:
@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。以下示例统计了前例中读取器(reader)中可用条目的数量:
int count = StepScopeTestUtils.doInStepScope(stepExecution,
new Callable<Integer>() {
public Integer call() throws Exception {
int count = 0;
while (reader.read() != null) {
count++;
}
return count;
}
});
模拟领域对象
在为Spring Batch组件编写单元测试和集成测试时,另一个常见问题是如何模拟领域对象。一个很好的例子是StepExecutionListener,如下面的代码片段所示:
public class NoWorkFoundStepExecutionListener implements StepExecutionListener {
public ExitStatus afterStep(StepExecution stepExecution) {
if (stepExecution.getReadCount() == 0) {
return ExitStatus.FAILED;
}
return null;
}
}
该框架提供了上述监听器示例,并检查 StepExecution 的读取计数是否为空,从而表明未执行任何工作。虽然这个示例相当简单,但它有助于说明在尝试对需要 Spring Batch 域对象的接口实现类进行单元测试时可能遇到的问题类型。请考虑以下针对前述示例中监听器的单元测试:
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。有了这个工厂,单元测试可以更新得更简洁,如下例所示:
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 中找到完整的方法列表。