1. 单元测试


XML

Java

与其他 application 样式一样,对作为批处理 job 的一部分编写的任何 code 进行单元测试非常重要。 Spring 核心文档详细介绍了如何使用 Spring 进行单元和 integration 测试,因此这里不再重复。然而,重要的是要考虑如何“端到端”测试批量 job,这是本章的内容。 spring-batch-test 项目包括促进此 end-to-end 测试方法的 classes。

1.1. 创建单元测试 Class

在单元测试的 order 中 run 批处理 job,framework 必须加载 job 的ApplicationContext。两个 annotations 用于触发此行为:

  • @RunWith(SpringRunner.class):表示 class 应该使用 Spring 的 JUnit 工具

  • @ContextConfiguration(…):表示配置ApplicationContext的资源。

从 v4.1 开始,还可以使用@SpringBatchTest annotation 在 test context 中 inject Spring Batch 测试实用程序,如JobLauncherTestUtilsJobRepositoryTestUtils

以下 example 显示了正在使用的注释:

使用 Java Configuration

@SpringBatchTest
@RunWith(SpringRunner.class)
@ContextConfiguration(classes=SkipSampleConfiguration.class)
public class SkipSampleFunctionalTests { ... }

使用 XML Configuration

@SpringBatchTest
@RunWith(SpringRunner.class)
@ContextConfiguration(locations = { "/simple-job-launcher-context.xml",
                                    "/jobs/skipSampleJob.xml" })
public class SkipSampleFunctionalTests { ... }

1.2. End-To-End 批处理作业的测试

“端到端”测试可以定义为从头到尾测试批 job 的完整 run。这允许进行测试以设置测试条件,执行 job 并验证最终结果。

在下面的示例中,批处理 job 从数据库读取并写入平面文件。测试方法首先使用测试数据设置数据库。它清除 CUSTOMER table 然后插入 10 条新记录。然后测试使用launchJob()方法启动JoblaunchJob()方法由JobLauncherTestUtils class 提供。 JobLauncherTestUtils class 还提供launchJob(JobParameters)方法,允许测试给出特定参数。 launchJob()方法返回JobExecution object,这对于断言有关Job run 的特定信息很有用。在以下情况中,测试验证Job以状态“COMPLETED”结束:

基于 XML 的 Configuration

@SpringBatchTest
@RunWith(SpringRunner.class)
@ContextConfiguration(locations = { "/simple-job-launcher-context.xml",
                                    "/jobs/skipSampleJob.xml" })
public class SkipSampleFunctionalTests {

    @Autowired
    private JobLauncherTestUtils jobLauncherTestUtils;

    private SimpleJdbcTemplate simpleJdbcTemplate;

    @Autowired
    public void setDataSource(DataSource dataSource) {
        this.simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource);
    }

    @Test
    public void testJob() throws Exception {
        simpleJdbcTemplate.update("delete from CUSTOMER");
        for (int i = 1; i <= 10; i++) {
            simpleJdbcTemplate.update("insert into CUSTOMER values (?, 0, ?, 100000)",
                                      i, "customer" + i);
        }

        JobExecution jobExecution = jobLauncherTestUtils.launchJob();

        Assert.assertEquals("COMPLETED", jobExecution.getExitStatus().getExitCode());
    }
}

基于 Java 的 Configuration

@SpringBatchTest
@RunWith(SpringRunner.class)
@ContextConfiguration(classes=SkipSampleConfiguration.class)
public class SkipSampleFunctionalTests {

    @Autowired
    private JobLauncherTestUtils jobLauncherTestUtils;

    private SimpleJdbcTemplate simpleJdbcTemplate;

    @Autowired
    public void setDataSource(DataSource dataSource) {
        this.simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource);
    }

    @Test
    public void testJob() throws Exception {
        simpleJdbcTemplate.update("delete from CUSTOMER");
        for (int i = 1; i <= 10; i++) {
            simpleJdbcTemplate.update("insert into CUSTOMER values (?, 0, ?, 100000)",
                                      i, "customer" + i);
        }

        JobExecution jobExecution = jobLauncherTestUtils.launchJob();

        Assert.assertEquals("COMPLETED", jobExecution.getExitStatus().getExitCode());
    }
}

1.3. 测试个别步骤

对于复杂的批处理作业,end-to-end 测试方法中的测试用例可能变得难以管理。在这些情况下,让测试用例自行测试各个步骤可能更有用。 JobLauncherTestUtils class 包含一个名为launchStep的方法,该方法采用 step name 并仅运行该特定的Step。这种方法允许更有针对性的测试,让测试只为那个 step 设置数据并直接验证其结果。以下 example 显示了如何使用launchStep方法加载Step by name:

JobExecution jobExecution = jobLauncherTestUtils.launchStep("loadFileStep");

1.4. 测试 Step-Scoped 组件

通常,在运行时为您的步骤配置的组件使用 step 作用域和晚 binding 来从 step 或 job 执行 inject context。除非您有办法将 context 设置为 step 执行,否则这些作为独立组件进行测试非常棘手。这是 Spring Batch 中的两个组件的目标:StepScopeTestExecutionListenerStepScopeTestUtils

listener 在 class level 中声明,其 job 是为每个测试方法创建一个 step 执行 context,如下面的 example 所示:

@ContextConfiguration
@TestExecutionListeners( { DependencyInjectionTestExecutionListener.class,
    StepScopeTestExecutionListener.class })
@RunWith(SpringRunner.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 framework,它处理从配置的 application context 到 reader 的 inject 的依赖注入。另一个是 Spring Batch StepScopeTestExecutionListener。它的工作原理是在测试用例中查找StepExecution的工厂方法,将其用作测试方法的 context,就像在运行时Step执行时一样 active。工厂方法由其签名检测(它必须 return a StepExecution)。如果未提供工厂方法,则会创建默认StepExecution

从 v4.1 开始,如果 test class 用@SpringBatchTest注释,StepScopeTestExecutionListenerJobScopeTestExecutionListener将作为测试执行 listeners 导入。前面的测试 example 可以配置如下:

@SpringBatchTest
@RunWith(SpringRunner.class)
@ContextConfiguration
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 范围的持续时间是测试方法的执行,则 listener 方法很方便。对于更灵活但更具侵入性的方法,您可以使用StepScopeTestUtils。以下 example 计算上一个 example 中显示的 reader 中可用的项目数:

int count = StepScopeTestUtils.doInStepScope(stepExecution,
    new Callable<Integer>() {
      public Integer call() throws Exception {

        int count = 0;

        while (reader.read() != null) {
           count++;
        }
        return count;
    }
});

1.5. 验证输出 Files

批处理 job 写入数据库时,很容易查询数据库以验证输出是否符合预期。但是,如果批处理 job 写入文件,则验证输出同样重要。 Spring Batch 提供了一个名为AssertFile的 class,以便于验证输出 files。名为assertFileEquals的方法需要两个File objects(或两个Resource objects)并通过 line 断言 line,这两个 files 具有相同的内容。因此,可以创建具有预期输出的文件,并将其与实际结果进行比较,如下面的示例所示:

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));

1.6. Mocking Domain Objects

为 Spring Batch 组件编写单元和 integration 测试时遇到的另一个常见问题是如何 mock domain objects。一个好的 example 是StepExecutionListener,如下面的 code 片段所示:

public class NoWorkFoundStepExecutionListener extends StepExecutionListenerSupport {

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

前面的 listener example 由 framework 提供,并检查StepExecution是否为空读取计数,从而表示没有完成任何工作。虽然这个例子非常简单,但它用于说明在尝试单元测试实现需要 Spring Batch domain objects 的接口的 classes 时可能遇到的问题类型。考虑前面的 example 中 listener 的以下单元测试:

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 domain model 遵循良好的 object-oriented 原则,所以StepExecution需要JobExecution,这需要JobInstanceJobParameters来创建有效的StepExecution。虽然这在一个可靠的 domain model 中是好的,但它确实使 creating stub objects 用于单元测试详细。为解决此问题,Spring Batch 测试模块包含 creating domain objects: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中找到。