Unit Testing

与其他应用风格一样,单元测试批处理作业的一部分编写的任何代码至关重要。Spring 核心文档详细介绍了如何使用 Spring 进行单元和集成测试,因此这里将不再赘述。但是,考虑如何“端到端”测试批处理作业非常重要,这也是本章涵盖的内容。spring-batch-test 项目包含有助于这种端到端测试方法的类。

Creating a Unit Test Class

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

  • @SpringJUnitConfig 表示类应该使用 Spring 的 JUnit 工具

  • @SpringBatchTest 在测试上下文中注入 Spring Batch 测试实用程序(例如 JobLauncherTestUtilsJobRepositoryTestUtils

如果测试上下文包含单个 Job Bean 定义,那么将在 JobLauncherTestUtils 中自动装配此 Bean。否则,应该在 JobLauncherTestUtils 上手动设置正在测试的作业。

Java

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

Using Java Configuration
@SpringBatchTest
@SpringJUnitConfig(SkipSampleConfiguration.class)
public class SkipSampleFunctionalTests { ... }
XML

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

Using XML Configuration
@SpringBatchTest
@SpringJUnitConfig(locations = { "/simple-job-launcher-context.xml",
                                    "/jobs/skipSampleJob.xml" })
public class SkipSampleFunctionalTests { ... }

End-To-End Testing of Batch Jobs

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

考虑读取数据库并写入平面文件的批处理作业的示例。测试方法从使用测试数据设置数据库开始。它将 CUSTOMER 表清除,然后插入 10 条新记录。然后测试通过使用 launchJob() 方法启动 JobJobLauncherTestUtils 类提供了 launchJob() 方法。JobLauncherTestUtils 类还提供了 launchJob(JobParameters) 方法,它允许测试给予特定参数。launchJob() 方法返回 JobExecution 对象,该对象对于断言有关 Job 运行的特定信息非常有用。在以下示例中,测试验证了 JobCOMPLETED 状态结束。

Java

以下清单展示了使用 Java 配置风格的 JUnit 5 的示例:

Java Based Configuration
@SpringBatchTest
@SpringJUnitConfig(SkipSampleConfiguration.class)
public class SkipSampleFunctionalTests {

    @Autowired
    private JobLauncherTestUtils jobLauncherTestUtils;

    private JdbcTemplate jdbcTemplate;

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

    @Test
    public void testJob(@Autowired Job job) throws Exception {
        this.jobLauncherTestUtils.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 = jobLauncherTestUtils.launchJob();


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

以下清单展示了使用 XML 配置风格的 JUnit 5 的示例:

XML Based Configuration
@SpringBatchTest
@SpringJUnitConfig(locations = { "/simple-job-launcher-context.xml",
                                    "/jobs/skipSampleJob.xml" })
public class SkipSampleFunctionalTests {

    @Autowired
    private JobLauncherTestUtils jobLauncherTestUtils;

    private JdbcTemplate jdbcTemplate;

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

    @Test
    public void testJob(@Autowired Job job) throws Exception {
        this.jobLauncherTestUtils.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 = jobLauncherTestUtils.launchJob();


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

Testing Individual Steps

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

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

Testing Step-Scoped Components

通常,在运行时为步骤配置的组件使用步骤作用域和延迟绑定来从步骤或作业执行注入上下文。这些组件很难作为独立组件进行测试,除非你有办法为这些组件设置上下文(就好像它们在一个步骤执行中一样)。这是 Spring Batch 中两个组件的目标:StepScopeTestExecutionListenerStepScopeTestUtils

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

@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 测试框架,它处理从配置的应用程序上下文中注入依存关系以便注入读取器。另一个是 Spring Batch StepScopeTestExecutionListener。它的工作原理是寻找一个 StepExecution 的测试用例中的工厂方法,并将其用作测试方法的上下文,就好像该执行在运行时在 Step 中处于活动状态一样。工厂方法是通过其签名检测到的(它必须返回一个 StepExecution)。如果没有提供工厂方法,便会创建一个默认的 StepExecution

从 v4.1 开始,如果测试类带有 @SpringBatchTest 注释,StepScopeTestExecutionListenerJobScopeTestExecutionListener 会被导入为测试执行侦听器。前面的测试示例可以配置为:

@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。以下示例会统计在前面示例中显示的读取器中可用的项目数量:

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

        int count = 0;

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

Validating Output Files

当一个批处理作业写入数据库时,很容易查询数据库以验证输出是否符合预期。但是,如果批处理作业写入文件,则同样重要的是验证输出。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));

Mocking Domain Objects

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

public class NoWorkFoundStepExecutionListener extends StepExecutionListenerSupport {

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

该框架提供了前面的侦听器示例,并针对空读计数检查 StepExecution,从而表明没有完成任何工作。虽然这个示例非常简单,但它有助于说明在尝试对实现需要 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 需要一个 JobExecutionJobExecution 需要一个 JobInstanceJobParameters,才能创建一个有效的 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中找到方法的完整列表。