10. 单元测试

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

10.1 创建单元测试 Class

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

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

  • @ContextConfiguration(locations = {...}):指示哪些 XML files 包含 ApplicationContext。

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

10.2 End-To-End 批量作业的测试

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

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

@RunWith(SpringJUnit4ClassRunner.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().getStatus();

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

10.3 测试个别步骤

对于复杂的批处理作业,end-to-end 测试方法中的测试用例可能变得难以管理。在这些情况下,让测试用例自行测试各个步骤可能更有用。 AbstractJobTests class 包含一个方法launchStep,该方法采用 step name 并仅运行该特定的Step。此方法允许测试为该 step 设置数据并直接验证其结果,从而允许更有针对性的测试。

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

10.4 测试 Step-Scoped 组件

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

listener 在 class level 中声明,其 job 是为每个测试方法创建一个 step 执行 context。例如:

@ContextConfiguration
@TestExecutionListeners( { DependencyInjectionTestExecutionListener.class,
    StepScopeTestExecutionListener.class })
@RunWith(SpringJUnit4ClassRunner.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 getStepExection() {
        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,另一个是 Spring Batch StepScopeTestExecutionListener。它的工作原理是在测试用例中查找StepExecution的工厂方法,并将其用作测试方法的 context,就像在运行时 Step 中的 active 执行一样。工厂方法由其签名检测(它只需要 return StepExecution)。如果未提供工厂方法,则会创建默认StepExecution

如果您希望 step 范围的持续时间是测试方法的执行,则 listener 方法很方便。对于更灵活但更具侵入性的方法,您可以使用StepScopeTestUtils。对于 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;
    }
});

10.5 验证输出 Files

批处理 job 写入数据库时,很容易查询数据库以验证输出是否符合预期。但是,如果批处理 job 写入文件,则验证输出同样重要。 Spring Batch 提供 class AssertFile以便于验证输出 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));

10.6 Mocking Domain Objects

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

public class NoWorkFoundStepExecutionListener extends StepExecutionListenerSupport {

    public ExitStatus afterStep(StepExecution stepExecution) {
        if (stepExecution.getReadCount() == 0) {
            throw new NoWorkFoundException("Step has not processed any items");
        }
        return stepExecution.getExitStatus();
    }
}

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

private NoWorkFoundStepExecutionListener tested = new NoWorkFoundStepExecutionListener();

@Test
public void testAfterStep() {
    StepExecution stepExecution = new StepExecution("NoProcessingStep",
                new JobExecution(new JobInstance(1L, new JobParameters(),
                                 "NoProcessingJob")));

    stepExecution.setReadCount(0);

    try {
        tested.afterStep(stepExecution);
        fail();
    } catch (NoWorkFoundException e) {
        assertEquals("Step has not processed any items", e.getMessage());
    }
}

因为 Spring Batch domain model 遵循良好的 object 定向原则,所以 StepExecution 需要JobExecution,这需要JobInstanceJobParameters in order 来创建有效的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.setReadCount(0);

    try {
        tested.afterStep(stepExecution);
        fail();
    } catch (NoWorkFoundException e) {
        assertEquals("Step has not processed any items", e.getMessage());
    }
}

用于创建简单StepExecution的上述方法只是工厂内可用的一种便捷方法。完整的方法列表可以在Javadoc中找到。

Updated at: 9 months ago
9.5. 声明性重试Table of content11. Common 批处理模式