单元测试
与其他应用程序样式一样,对作为批处理作业一部分编写的任何代码进行单元测试都极其重要。Spring 核心文档详细介绍了如何使用 Spring 进行单元测试和集成测试,因此此处不再重复。然而,重要的是考虑如何对批处理作业进行“端到端”测试,本章将涵盖这一点。spring-batch-test
项目包含促进此端到端测试方法的类。
创建单元测试类
为了让单元测试运行批处理作业,框架必须加载作业的 ApplicationContext
。使用两个注解来触发此行为
-
@SpringJUnitConfig
表示该类应使用 Spring 的 JUnit 功能 -
@SpringBatchTest
将 Spring Batch 测试工具(如JobLauncherTestUtils
和JobRepositoryTestUtils
)注入测试上下文
如果测试上下文包含单个 Job bean 定义,则此 bean 将自动注入 JobLauncherTestUtils 。否则,应手动将待测试的作业设置到 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
以下列表显示了使用 JUnit 5 和 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());
}
}
以下列表显示了使用 JUnit 5 和 XML 配置风格的示例
@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());
}
}
测试单个 Step
对于复杂的批处理作业,端到端测试方法中的测试用例可能变得难以管理。在这种情况下,为单独测试单个 Step 设置测试用例可能更有用。JobLauncherTestUtils
类包含一个名为 launchStep
的方法,该方法接受一个 Step 名称并仅运行该特定 Step
。这种方法允许进行更有针对性的测试,使测试能够仅为该 Step 设置数据并直接验证其结果。以下示例显示了如何使用 launchStep
方法按名称加载 Step
JobExecution jobExecution = jobLauncherTestUtils.launchStep("loadFileStep");
测试 Step 范围内的组件
通常,为 Step 配置的运行时组件会使用 Step 范围和后期绑定来注入来自 Step 或 Job 执行的上下文。这些组件作为独立组件难以测试,除非您有一种方法可以设置上下文,使其就像在 Step 执行中一样。这就是 Spring Batch 中两个组件的目标:StepScopeTestExecutionListener
和 StepScopeTestUtils
。
监听器在类级别声明,其作用是为每个测试方法创建一个 Step 执行上下文,如下例所示
@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());
}
}
如果您希望 Step 范围的持续时间与测试方法的执行时间一致,则监听器方法很方便。对于更灵活但更具侵入性的方法,您可以使用 `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 中找到完整的方法列表。