10. Unit Testing

与其他应用程序样式一样,对作为批处理作业一部分编写的任何代码进行单元测试也非常重要。 Spring 核心文档详细介绍了如何对 Spring 进行单元测试和集成测试,因此在此不再赘述。但是,重要的是考虑如何“端对端”测试批处理作业,这是本章将重点讨论的内容。Spring Batch 测试 Item 包括有助于简化这种端到端测试方法的类。

10.1 创建单元测试类

为了使单元测试运行批处理作业,框架必须加载作业的 ApplicationContext。使用两个 Comments 来触发此操作:

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

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

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

10.2 批处理作业的端到端测试

“端到端”测试可以定义为从头到尾测试批处理作业的完整运行。这样就可以进行测试,以设置测试条件,执行工作并验证最终结果。

在下面的示例中,批处理作业从数据库读取并写入平面文件。测试方法首先使用测试数据构建数据库。它将清除 CUSTOMER 表,然后插入 10 条新记录。然后,测试使用launchJob()方法启动JoblaunchJob()方法由JobLauncherTestUtils类提供。 utils 类还提供了launchJob(JobParameters),它允许测试提供特定的参数。 launchJob()方法返回JobExecution对象,该对象对于 assert 有关Job运行的特定信息很有用。在以下情况下,测试将验证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 测试各个步骤

对于复杂的批处理作业,端到端测试方法中的测试用例可能变得难以 Management。在这些情况下,使用测试用例自行测试各个步骤可能会更有用。 AbstractJobTests类包含方法launchStep,该方法采用步骤名称并仅运行特定的Step。通过允许测试仅针对该步骤设置数据并直接验证其结果,该方法可以进行更具针对性的测试。

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

10.4 测试步骤范围的组件

通常,在运行时为您的步骤配置的组件会使用步骤作用域和后期绑定来从步骤或作业执行中注入上下文。将它们作为独立的组件进行测试非常棘手,除非您有一种将上下文设置为好像它们在一步执行中的方式。这就是 Spring Batch 中两个组件的目标:StepScopeTestExecutionListenerStepScopeTestUtils

侦听器在类级别声明,其工作是为每个测试方法创建一个步骤执行上下文。例如:

@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 框架,并处理来自已配置的应用程序上下文的依赖项注入,从而注入读取器,另一个是 Spring Batch StepScopeTestExecutionListener。它通过在StepExecution的测试用例中查找工厂方法并将其用作测试方法的上下文来工作,就像该执行在运行时在 Step 中处于活动状态一样。 factory 方法通过其签名检测(它只需返回StepExecution)。如果未提供工厂方法,则会创建默认的StepExecution

如果您希望步骤作用域的持续时间是测试方法的执行,则侦听器方法很方便。对于更灵活但更具侵入性的方法,您可以使用StepScopeTestUtils。例如,要计算上面的 Reader 中可用的 Item 数:

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 验证输出文件

当批处理作业写入数据库时,很容易查询数据库以验证输出是否符合预期。但是,如果批处理作业写入文件,则验证输出同样重要。 Spring Batch 提供了一个AssertFile类,以方便验证输出文件。方法assertFileEquals接受两个File对象(或两个Resource对象),并逐行 assert 两个文件具有相同的内容。因此,可以创建具有预期输出的文件并将其与实际结果进行比较:

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 模拟域对象

在编写 Spring Batch 组件的单元测试和集成测试时遇到的另一个常见问题是如何模拟域对象。一个很好的例子是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();
    }
}

上面的侦听器由框架提供,并检查StepExecution的空读取计数,从而表示未完成任何工作。尽管这个示例非常简单,但是它用于说明尝试对实现需要 Spring Batch 域对象的接口的类进行单元测试时可能遇到的问题类型。考虑上面的侦听器的单元测试:

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 域模型遵循良好的面向对象原则,因此 StepExecution 需要JobExecution,而后者需要JobInstanceJobParameters才能创建有效的StepExecution。尽管这在固态域模型中很好,但确实会使创建用于单元测试的存根对象变得冗长。为了解决这个问题,Spring Batch 测试模块包括一个用于创建域对象的工厂: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中找到。