单元测试

本站( springdoc.cn )中的内容来源于 spring.io ,原始版权归属于 spring.io。由 springdoc.cn 进行翻译,整理。可供个人学习、研究,未经许可,不得进行任何转载、商用或与之相关的行为。 商标声明:Spring 是 Pivotal Software, Inc. 在美国以及其他国家的商标。

与其他应用程序样式一样,对作为批 batch job 一部分编写的任何代码进行单元测试极为重要。Spring 核心文档详细介绍了如何使用 Spring 进行单元测试和集成测试,在此不再赘述。不过,重要的是要考虑如何对 batch job 进行 "端到端" 测试,这也是本章的内容。spring-batch-test 项目中包含的类有助于采用这种端到端测试方法。

创建单元测试类

要让单元测试运行 batch job,框架必须加载 job 的 ApplicationContext。有两个注解用于触发这一行为:

  • @SpringJUnitConfig 表明类应该使用 Spring 的 JUnit 设施

  • @SpringBatchTest 在测试上下文中注入 Spring Batch test 工具(如 JobLauncherTestUtilsJobRepositoryTestUtils)。

如果测试上下文包含单个 Job bean 定义,该 bean 将在 JobLauncherTestUtils 中自动装配。否则,应在 JobLauncherTestUtils 中手动设置被测 job。

下面的 Java 示例展示了注解的使用:

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

下面的 XML 示例显示了使用中的注解:

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

Batch Job 的端到端测试

“端到端” 测试可定义为从头到尾测试 batch job 的整个运行过程。这样就可以进行设置测试条件、执行 job 和验证最终结果的测试。

请看一个从数据库读取数据并写入平面文件的 batch job 示例。测试方法首先用测试数据设置数据库。它会清除 CUSTOMER 表,然后插入 10 条新记录。然后,测试使用 launchJob() 方法启动 JoblaunchJob() 方法由 JobLauncherTestUtils 类提供。JobLauncherTestUtils 类还提供了 launchJob(JobParameters) 方法,该方法可让测试给出特定参数。launchJob() 方法会返回 JobExecution 对象,这对于断言有关 Job 运行的特定信息非常有用。在下面的案例中,测试验证了 JobCOMPLETED 状态结束。

下面的列表显示了采用 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());
    }
}

下面的列表显示了一个采用 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());
    }
}

测试各个 Step

对于复杂的 batch job,端到端测试方法中的测试用例可能会变得难以管理。在这种情况下,使用测试用例单独测试单个 step 可能更有用。JobLauncherTestUtils 类包含一个名为 launchStep 的方法,该方法接收 step 名称并只运行该特定 Step。这种方法允许进行更有针对性的测试,让测试只为该 step 设置数据,并直接验证其结果。下面的示例展示了如何使用 launchStep 方法按 Step 名称加载 Step

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

测试 Step Scope 组件

通常,在运行时为 step 配置的组件会使用 step scope 和延迟绑定来注入 step 或 job execution 的 context。这些组件作为独立组件进行测试非常棘手,除非你有办法将 context 设置为 step execution 中的 context。这就是 Spring Batch 中 StepScopeTestExecutionListenerStepScopeTestUtils 两个组件的目标。

listener 在类级别声明,其任务是为每个测试方法创建一个 step execution context,如下例所示:

@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 测试框架,它负责从配置的 application context 中进行依赖注入,以便注入 reader。另一个是 Spring Batch 的 StepScopeTestExecutionListener。它通过在测试用例中查找一个用于 StepExecution 的工厂方法,并将其作为测试方法的 context,就像在 execution 时活动的 Step 中一样。工厂方法通过其签名进行检测(必须返回一个 StepExecution)。如果没有提供工厂方法,则会创建一个默认的 StepExecution

从 v4.1 开始,如果测试类使用 @SpringBatchTest 注解,StepScopeTestExecutionListenerJobScopeTestExecutionListener 将作为测试 execution listener 导入。可以按如下方式配置前面的测试示例:

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

}

如果你希望 step scope 的持续时间就是测试方法的执行时间,那么监听器方法就很方便。如果想采用更灵活但更具侵入性的方法,可以使用 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;
    }
});

验证输出文件

当 batch job 写入数据库时,很容易通过查询数据库来验证输出是否符合预期。但是,如果 batch job 写入文件,对输出进行验证也同样重要。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));

模拟 Domain 对象

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

public class NoWorkFoundStepExecutionListener extends StepExecutionListenerSupport {

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

该框架提供了前面的 listener 示例,并检查 StepExecution 的读取计数是否为空,从而表明没有完成任何工作。虽然这个示例相当简单,但它有助于说明在尝试对实现需要 Spring Batch domain 对象的接口的类进行单元测试时可能会遇到的问题类型。请看下面针对上例中 listener 的单元测试:

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 domain 模型遵循良好的面向对象原则,因此 StepExecution 需要一个 JobExecution,而 JobExecution 需要一个 JobInstanceJobParameters,这样才能创建一个有效的 StepExecution。虽然这在稳固的 domain 模型中是个好办法,但它确实使创建用于单元测试的 stub 对象变得繁琐。为了解决这个问题,Spring Batch 测试模块包含了一个用于创建 domain 对象的工厂: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 中找到完整的方法列表。