单元测试

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

创建单元测试类

为了让单元测试运行批处理作业,框架必须加载作业的 ApplicationContext。两个注释用于触发此行为:

  • @RunWith(SpringJUnit4ClassRunner.class): 表示该类应该使用 Spring 的 JUnit 设施

  • @ContextConfiguration(…​):指示要配置哪些资源 ApplicationContext

从 v4.1 开始,还可以使用注解在测试上下文中注入 Spring Batch 测试实用程序,如JobLauncherTestUtils和。JobRepositoryTestUtils@SpringBatchTest

需要注意的是JobLauncherTestUtils需要一个Jobbean,而那个 JobRepositoryTestUtils需要一个DataSourcebean。由于在测试上下文中@SpringBatchTest 注册了 aJobLauncherTestUtils和 a JobRepositoryTestUtils,因此预计测试上下文包含 aJob和 a的单个自动装配候选者DataSource(单个 bean 定义或带有注释的 bean 定义org.springframework.context.annotation.Primary)。

以下 Java 示例显示了正在使用的注解:

使用 Java 配置
@SpringBatchTest
@RunWith(SpringRunner.class)
@ContextConfiguration(classes=SkipSampleConfiguration.class)
public class SkipSampleFunctionalTests { ... }

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

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

批处理作业的端到端测试

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

考虑一个从数据库读取并写入平面文件的批处理作业示例。测试方法首先使用测试数据设置数据库。它清除 CUSTOMER 表,然后插入 10 条新记录。然后测试Job通过使用该 launchJob()方法启动。该launchJob()方法由JobLauncherTestUtils 类提供。该类JobLauncherTestUtils还提供了launchJob(JobParameters) 方法,该方法允许测试给出特定的参数。该launchJob()方法返回JobExecution对象,这对于断言有关Job运行的特定信息很有用。在以下情况下,测试验证Job以状态“完成”结束。

以下清单显示了 XML 中的示例:

基于 XML 的配置
@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 中的示例:

基于 Java 的配置
@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());
    }
}

测试单个步骤

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

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

测试步骤范围的组件

通常,在运行时为您的步骤配置的组件使用步骤范围和后期绑定从步骤或作业执行中注入上下文。这些作为独立组件进行测试是很棘手的,除非您有办法设置上下文,就好像它们在单步执行中一样。这是 Spring Batch 中两个组件的目标: StepScopeTestExecutionListenerStepScopeTestUtils.

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

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

从 v4.1 开始,StepScopeTestExecutionListener如果 JobScopeTestExecutionListener测试类使用@SpringBatchTest. 前面的测试示例可以配置如下:

@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());
    }

}

如果您希望步骤范围的持续时间是测试方法的执行,则侦听器方法很方便。对于更灵活但更具侵入性的方法,您可以使用StepScopeTestUtils. 以下示例计算上一个示例中显示的阅读器中可用的项目数:

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 提供了一个类AssertFile ,用于方便验证输出文件。调用的方法assertFileEquals接受两个File对象(或两个Resource对象)并逐行断言这两个文件具有相同的内容。因此,可以创建具有预期输出的文件并将其与实际结果进行比较,如下例所示:

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

模拟域对象

为 Spring Batch 组件编写单元和集成测试时遇到的另一个常见问题是如何模拟域对象。一个很好的例子是 a StepExecutionListener,如以下代码片段所示:

public class NoWorkFoundStepExecutionListener extends StepExecutionListenerSupport {

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

前面的侦听器示例由框架提供,并检查 aStepExecution 的读取计数是否为空,因此表示没有完成任何工作。虽然此示例相当简单,但它用于说明在尝试对实现需要 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需要 a JobExecution,这需要一个JobInstanceand 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中找到完整的方法列表。


1. see XML Configuration