单元测试

单元测试

与其他应用程序样式一样,对编写的任何代码进行单元测试非常重要 作为批处理作业的一部分。Spring 核心文档介绍了如何进行单元和集成 test 与 Spring 一起进行非常详细的测试,因此这里不再重复。然而,重要的是, 考虑如何 “端到端” 测试 Batch 作业,这就是本章介绍的内容。 这spring-batch-testproject 包含有助于此端到端测试的类 方法。spring-doc.cadn.net.cn

创建 Unit Test 类

要使单元测试运行批处理作业,框架必须加载作业的ApplicationContext.两个注释用于触发此行为:spring-doc.cadn.net.cn

  • @SpringJUnitConfig指示该类应使用 Spring 的 JUnit 工具spring-doc.cadn.net.cn

  • @SpringBatchTest注入 Spring Batch 测试实用程序(例如JobLauncherTestUtilsJobRepositoryTestUtilsspring-doc.cadn.net.cn

如果测试上下文包含单个Jobbean 定义,这个 bean 将被自动装配JobLauncherTestUtils.否则,作业 Under Test 应在JobLauncherTestUtils.

下面的 Java 示例显示了正在使用的注释:spring-doc.cadn.net.cn

使用 Java 配置
@SpringBatchTest
@SpringJUnitConfig(SkipSampleConfiguration.class)
public class SkipSampleFunctionalTests { ... }

下面的 XML 示例显示了正在使用的注释:spring-doc.cadn.net.cn

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

批处理作业的端到端测试

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

考虑一个从数据库中读取并写入平面文件的批处理作业示例。 测试方法首先使用测试数据设置数据库。它会清除CUSTOMER表,然后插入 10 条新记录。然后,测试会启动Job通过使用launchJob()方法。这launchJob()method 由JobLauncherTestUtils类。这JobLauncherTestUtils类还提供了launchJob(JobParameters)方法,它允许测试给出特定的参数。这launchJob()方法 返回JobExecutionobject,这对于断言特定信息很有用 关于Job跑。在以下情况下,测试验证Job结尾为 状态为COMPLETED.spring-doc.cadn.net.cn

下面的清单显示了 XML 配置样式的 JUnit 5 的示例:spring-doc.cadn.net.cn

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

下面的清单显示了 Java 配置样式的 JUnit 5 的示例:spring-doc.cadn.net.cn

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

测试各个步骤

对于复杂的批处理作业,端到端测试方法中的测试用例可能会变为 不可收拾。在这些情况下,使用测试用例来测试个人可能更有用 他们自己走。这JobLauncherTestUtils类包含一个名为launchStep, 它采用步骤名称并仅运行该特定Step.这种方法允许 更有针对性的测试:让测试仅为该步骤设置数据并验证其 结果。以下示例演示如何使用launchStep方法加载Step按名称:spring-doc.cadn.net.cn

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

测试 Step-Scoped 组件

通常,在运行时为步骤配置的组件使用 step scope 和 late 绑定,用于从步骤或作业执行中注入上下文。这些很难测试,因为 独立组件,除非你有办法将上下文设置为一个步骤中 执行。这是 Spring Batch 中两个组件的目标:StepScopeTestExecutionListenerStepScopeTestUtils.spring-doc.cadn.net.cn

侦听器在类级别声明,其工作是创建步骤执行 context 中,如下例所示:spring-doc.cadn.net.cn

@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 Test 框架,它 处理来自已配置应用程序上下文的依赖项注入以注入读取器。 另一个是 Spring BatchStepScopeTestExecutionListener.它的工作原理是查找 factory 方法的测试用例中StepExecution,将其用作 test 方法,就好像该执行在Step在运行时。工厂方法 通过其签名检测到(它必须返回一个StepExecution).如果工厂方法是 not provided,则默认为StepExecution已创建。spring-doc.cadn.net.cn

从 v4.1 开始,StepScopeTestExecutionListenerJobScopeTestExecutionListener作为测试执行侦听器导入 如果测试类带有@SpringBatchTest.前面的测试 example 可以按如下方式配置:spring-doc.cadn.net.cn

@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.以下示例计算 上一个示例中显示的 Reader:spring-doc.cadn.net.cn

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对象)并逐行断言这两个 文件具有相同的内容。因此,可以创建具有预期 output 并将其与实际结果进行比较,如下例所示:spring-doc.cadn.net.cn

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 编写单元和集成测试时遇到的另一个常见问题 components 是如何模拟域对象。一个很好的例子是StepExecutionListener如 以下代码片段显示:spring-doc.cadn.net.cn

public class NoWorkFoundStepExecutionListener extends StepExecutionListenerSupport {

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

框架提供了前面的侦听器示例,并检查了StepExecution对于空 read 计数,因此表示未完成任何工作。虽然此示例是 相当简单,它用于说明您在 您尝试对实现需要 Spring Batch 域的接口的类进行单元测试 对象。请考虑对前面示例中的侦听器进行以下单元测试:spring-doc.cadn.net.cn

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需要JobExecution,这需要JobInstanceJobParameters,以创建有效的StepExecution.虽然这在坚实的领域中是好的 模型,它确实使创建用于单元测试的存根对象变得冗长。为了解决这个问题, Spring Batch 测试模块包括一个用于创建域对象的工厂:MetaDataInstanceFactory.给定此工厂,可以将单元测试更新为更多 简洁,如下例所示:spring-doc.cadn.net.cn

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 中找到完整的方法列表。spring-doc.cadn.net.cn