单元测试

与其他应用程序风格一样,对作为批处理作业一部分编写的任何代码进行单元测试至关重要。Spring 核心文档非常详细地介绍了如何使用 Spring 进行单元测试和集成测试,因此此处不再赘述。然而,重要的是要考虑如何对批处理作业进行“端到端”测试,这正是本章要涵盖的内容。spring-batch-test 项目包含了一些类,旨在促进这种端到端测试方法。spring-doc.cadn.net.cn

创建单元测试类

为了运行批处理作业的单元测试,框架必须加载作业的 ApplicationContext。使用两个注解来触发此行为:spring-doc.cadn.net.cn

  • @SpringJUnitConfig 表示该类应使用 Spring 的 JUnit 工具spring-doc.cadn.net.cn

  • @SpringBatchTest 在测试上下文中注入 Spring Batch 测试工具(例如 JobOperatorTestUtilsJobRepositoryTestUtilsspring-doc.cadn.net.cn

如果测试上下文包含单个 Job Bean 定义,则该 Bean 将自动注入到 JobOperatorTestUtils 中。否则,应手动在 JobOperatorTestUtils 上设置待测试的作业。
自 Spring Batch 6.0 起,不再支持 JUnit 4。建议迁移至 JUnit Jupiter。

以下 Java 示例展示了注解的使用:spring-doc.cadn.net.cn

使用 Java 配置
@SpringBatchTest
@SpringJUnitConfig(SkipSampleConfiguration.class)
public class SkipSampleFunctionalTests { ... }

以下 XML 示例展示了注解的使用:spring-doc.cadn.net.cn

使用 XML 配置
@SpringBatchTest
@SpringJUnitConfig(locations = { "/skip-sample-configuration.xml" })
public class SkipSampleFunctionalTests { ... }

批处理作业的端到端测试

“端到端”测试可以定义为从开始到结束测试批处理作业的完整运行过程。这允许设置测试条件、执行作业并验证最终结果的测试。spring-doc.cadn.net.cn

考虑一个批处理作业的示例,该作业从数据库读取数据并写入平面文件。 测试方法首先通过设置测试数据来准备数据库。它会清空 CUSTOMER 表,然后插入 10 条新记录。接着,测试使用 startJob() 方法启动 JobstartJob() 方法由 JobOperatorTestUtils 类提供。JobOperatorTestUtils 类还提供 startJob(JobParameters) 方法,该方法允许测试传入特定参数。startJob() 方法返回 JobExecution 对象,这对于断言有关 Job 运行的特定信息非常有用。在以下情况中,测试验证 Job 是否以 COMPLETED 的状态结束。spring-doc.cadn.net.cn

以下列表展示了使用 Java 配置风格的 JUnit 5 示例:spring-doc.cadn.net.cn

基于 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 配置风格示例:spring-doc.cadn.net.cn

基于 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 方法按名称启动一个 Stepspring-doc.cadn.net.cn

JobExecution jobExecution = jobOperatorTestUtils.startStep("loadFileStep");

测试步骤作用域组件

通常,为您的步骤在运行时配置的组件会使用步骤作用域和延迟绑定,以注入来自步骤或作业执行的上下文。作为独立组件,这些很难进行测试,除非您有一种方法可以像在步骤执行中那样设置上下文。这正是 Spring Batch 中两个组件的目标: StepScopeTestExecutionListenerStepScopeTestUtilsspring-doc.cadn.net.cn

监听器在类级别声明,其任务是为每个测试方法创建一个步骤执行上下文,如下例所示:spring-doc.cadn.net.cn

@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 框架,它负责从配置的应用上下文中进行依赖注入以注入读取器。另一个是 Spring Batch StepScopeTestExecutionListener。它通过在测试用例中查找用于StepExecution的工厂方法来工作,并将该方法作为测试方法的上下文,就像该执行在运行时处于Step中一样。工厂方法通过其签名被检测到(它必须返回一个StepExecution)。如果未提供工厂方法,则会创建一个默认的StepExecutionspring-doc.cadn.net.cn

从 v4.1 开始,如果测试类使用了 @SpringBatchTest 注解,则 StepScopeTestExecutionListenerJobScopeTestExecutionListener 会被导入为测试执行监听器。前面的测试示例可以按如下方式配置:spring-doc.cadn.net.cn

@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。以下示例统计了前一个示例中读取器可用的项目数量:spring-doc.cadn.net.cn

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,如下面的代码片段所示:spring-doc.cadn.net.cn

public class NoWorkFoundStepExecutionListener implements StepExecutionListener {

    public ExitStatus afterStep(StepExecution stepExecution) {
        if (stepExecution.getReadCount() == 0) {
            return ExitStatus.FAILED;
        }
        return null;
    }
}

该框架提供了前述的监听器示例,并检查 StepExecution 以判断读取计数是否为空,从而表明未执行任何工作。虽然此示例相当简单,但它有助于说明在尝试对实现需要 Spring Batch 领域对象的接口的类进行单元测试时可能遇到的各类问题。请考虑以下针对前述示例中监听器的单元测试:spring-doc.cadn.net.cn

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,而该对象又需要一个 JobInstanceJobParameters,才能创建一个有效的 StepExecution。虽然这在坚实的领域模型中是好的做法, 但它确实使得为单元测试创建桩对象变得冗长。为解决此问题, Spring Batch 测试模块包含一个用于创建领域对象的工厂: MetaDataInstanceFactory。借助此工厂,单元测试可以更新得更加简洁,如下例所示:spring-doc.cadn.net.cn

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 中找到完整的方法列表。spring-doc.cadn.net.cn