单元测试
与其他应用程序风格一样,为批处理任务编写的所有代码进行单元测试极为重要。Spring核心文档详细介绍了如何使用Spring进行单元测试和集成测试,因此这里不再重复。然而,重要的是要考虑如何对批处理任务进行“端到端”测试,这也是本章要涵盖的内容。spring-batch-test
项目包含了一些类,这些类有助于实现这种端到端测试方法。
创建单元测试类
为了运行批处理作业的单元测试,框架必须加载作业的 ApplicationContext
。使用两种注解来触发此行为:
-
@SpringJUnitConfig
表示该类应使用 Spring 的 JUnit 功能 -
@SpringBatchTest
会在测试上下文中注入 Spring Batch 测试工具(例如JobLauncherTestUtils
和JobRepositoryTestUtils
)
如果测试上下文中包含一个 Job
bean 定义,此 bean 将会被自动注入到 JobLauncherTestUtils
中。否则,应该手动将被测试的 job 设置到 JobLauncherTestUtils
上。
- Java
- XML
下面的 Java 示例展示了注解的使用:
@SpringBatchTest
@SpringJUnitConfig(SkipSampleConfiguration.class)
public class SkipSampleFunctionalTests { ... }
下面的 XML 示例展示了注解的使用:
@SpringBatchTest
@SpringJUnitConfig(locations = { "/simple-job-launcher-context.xml",
"/jobs/skipSampleJob.xml" })
public class SkipSampleFunctionalTests { ... }
批处理作业的端到端测试
“端到端”测试可以定义为从开始到结束对批处理作业的完整运行进行测试。这使得测试可以设置测试条件、执行作业并验证最终结果。
考虑一个从数据库读取数据并写入平面文件的批处理作业示例。测试方法首先通过设置包含测试数据的数据库来开始。它清空 CUSTOMER
表,然后插入 10 条新记录。然后测试通过使用 launchJob()
方法启动 Job
。launchJob()
方法由 JobLauncherTestUtils
类提供。JobLauncherTestUtils
类还提供了 launchJob(JobParameters)
方法,这使得测试可以提供特定的参数。launchJob()
方法返回 JobExecution
对象,这对于断言 Job
运行的特定信息非常有用。在以下情况下,测试验证了 Job
是否以 COMPLETED
状态结束。
- Java
- XML
下面的代码示例展示了使用 Java 配置风格的 JUnit 5 示例:
@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 配置风格的 JUnit 5 示例:
@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());
}
}
测试单独的步骤
对于复杂的批处理任务,端到端测试方法中的测试用例可能会变得难以管理。在这些情况下,为每个单独的步骤编写测试用例可能更有用。JobLauncherTestUtils
类中包含一个名为 launchStep
的方法,该方法接受一个步骤名称,并仅运行该特定的 Step
。这种方法允许进行更 targeted(针对性)的测试,使测试只为该步骤设置数据,并直接验证其结果。以下示例展示了如何使用 launchStep
方法通过名称加载一个 Step
:
JobExecution jobExecution = jobLauncherTestUtils.launchStep("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());
}
}
有两种 TestExecutionListeners
。一种是普通的 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
。以下示例统计上一个示例中显示的读取器中可用项的数量:
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 提供了一个名为 AssertFile
的类,用于简化输出文件的验证过程。方法 assertFileEquals
接受两个 File
对象(或两个 Resource
对象),并逐行断言这两个文件的内容是否相同。因此,可以创建一个包含预期输出的文件,并将其与实际结果进行比较,如下例所示:
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));
模拟域对象
在为 Spring Batch 组件编写单元测试和集成测试时,遇到的另一个常见问题是如何模拟领域对象。一个很好的例子是 StepExecutionListener
,如下代码片段所示:
public class NoWorkFoundStepExecutionListener extends StepExecutionListenerSupport {
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 中找到完整的方法列表。