# 单元测试

## 单元测试

XMLJavaBoth

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

### 创建单元测试类

为了让单元测试运行批处理作业，框架必须加载作业的应用上下文。使用两个注释来触发此行为：

* `@RunWith(SpringJUnit4ClassRunner.class)`:表示类应该使用 Spring 的 JUnit 工具

* `@ContextConfiguration(…​)`:指示使用哪些资源配置`ApplicationContext`。

从 V4.1 开始，还可以使用`@SpringBatchTest`注释在测试上下文中注入 Spring 批测试实用程序，如`JobLauncherTestUtils`和`JobRepositoryTestUtils`。

|   |需要注意的是，`JobLauncherTestUtils`需要`Job` Bean，`JobRepositoryTestUtils`需要`DataSource` Bean。由于`@SpringBatchTest`在测试<br/>上下文中注册了一个`JobLauncherTestUtils`和一个`JobRepositoryTestUtils`，因此预计测试上下文包含一个用于`Job`和`DataSource`的单独的 AutoWire 候选项<br/>（要么是一个单独的 Bean 定义，要么是<br/>注释为`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 个新记录。然后，测试使用`launchJob()`方法启动`Job`。`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 批处理中两个组件的目标：`StepScopeTestExecutionListener`和`StepScopeTestUtils`。

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

```
@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 测试框架，它处理从配置的应用程序上下文注入的依赖项，以注入读取器。另一个是 Spring 批`StepScopeTestExecutionListener`。它的工作方式是在`StepExecution`的测试用例中寻找工厂方法，并将其用作测试方法的上下文，就好像该执行在运行时在`Step`中是活动的一样。通过其签名来检测工厂方法（它必须返回`StepExecution`）。如果没有提供工厂方法，则创建一个默认的`StepExecution`。

从 V4.1 开始，如果测试类被注释为`@SpringBatchTest`，则将`StepScopeTestExecutionListener`和`JobScopeTestExecutionListener`作为测试执行侦听器导入。前面的测试示例可以配置如下：

```
@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 提供了一个名为的类，以便于对输出文件进行验证。名为`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 批处理组件编写单元和集成测试时遇到的另一个常见问题是如何模拟域对象。一个很好的例子是`StepExecutionListener`，如以下代码片段所示：

```
public class NoWorkFoundStepExecutionListener extends StepExecutionListenerSupport {

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

前面的侦听器示例是由框架提供的，它检查`StepExecution`是否有空读计数，因此表示没有完成任何工作。虽然这个示例相当简单，但它用于说明在试图对实现需要 Spring 批处理域对象的接口的测试类进行单元测试时可能遇到的问题类型。在前面的示例中，考虑下面的监听器单元测试：

```
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 批处理域模型遵循良好的面向对象原则，所以`StepExecution`需要一个`JobExecution`，这需要一个`JobInstance`和`JobParameters`，以创建一个有效的`StepExecution`。虽然这在固态域模型中很好，但它确实使为单元测试创建存根对象变得非常详细。为了解决这个问题， Spring 批测试模块包括一个用于创建域对象的工厂：`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](https://docs.spring.io/spring-batch/apidocs/org/springframework/batch/test/MetaDataInstanceFactory.html)中找到。