單元測試

與其他應用程式樣式一樣,對作為批處理作業一部分編寫的任何程式碼進行單元測試都極其重要。Spring 核心文件詳細介紹瞭如何使用 Spring 進行單元測試和整合測試,因此此處不再重複。然而,重要的是考慮如何對批處理作業進行“端到端”測試,本章將涵蓋這一點。spring-batch-test 專案包含促進此端到端測試方法的類。

建立單元測試類

為了讓單元測試執行批處理作業,框架必須載入作業的 ApplicationContext。使用兩個註解來觸發此行為

  • @SpringJUnitConfig 表示該類應使用 Spring 的 JUnit 功能

  • @SpringBatchTest 將 Spring Batch 測試工具(如 JobLauncherTestUtilsJobRepositoryTestUtils)注入測試上下文

如果測試上下文包含單個 Job bean 定義,則此 bean 將自動注入 JobLauncherTestUtils。否則,應手動將待測試的作業設定到 JobLauncherTestUtils 上。
  • Java

  • XML

以下 Java 示例展示了註解的用法

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

以下 XML 示例展示了註解的用法

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

批處理作業的端到端測試

“端到端”測試可以定義為測試批處理作業從開始到結束的完整執行。這允許進行設定測試條件、執行作業並驗證最終結果的測試。

考慮一個批處理作業的示例,該作業從資料庫讀取並寫入平面檔案。測試方法首先透過測試資料設定資料庫。它清空 CUSTOMER 表,然後插入 10 條新記錄。然後,測試使用 launchJob() 方法啟動 JoblaunchJob() 方法由 JobLauncherTestUtils 類提供。JobLauncherTestUtils 類還提供了 launchJob(JobParameters) 方法,該方法允許測試傳遞特定引數。launchJob() 方法返回 JobExecution 物件,該物件對於斷言關於 Job 執行的特定資訊非常有用。在以下示例中,測試驗證 JobCOMPLETED 狀態結束。

  • Java

  • XML

以下列表顯示了使用 JUnit 5 和 Java 配置風格的示例

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

以下列表顯示了使用 JUnit 5 和 XML 配置風格的示例

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

測試單個 Step

對於複雜的批處理作業,端到端測試方法中的測試用例可能變得難以管理。在這種情況下,為單獨測試單個 Step 設定測試用例可能更有用。JobLauncherTestUtils 類包含一個名為 launchStep 的方法,該方法接受一個 Step 名稱並僅執行該特定 Step。這種方法允許進行更有針對性的測試,使測試能夠僅為該 Step 設定資料並直接驗證其結果。以下示例顯示瞭如何使用 launchStep 方法按名稱載入 Step

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

測試 Step 範圍內的元件

通常,為 Step 配置的執行時元件會使用 Step 範圍和後期繫結來注入來自 Step 或 Job 執行的上下文。這些元件作為獨立元件難以測試,除非您有一種方法可以設定上下文,使其就像在 Step 執行中一樣。這就是 Spring Batch 中兩個元件的目標:StepScopeTestExecutionListenerStepScopeTestUtils

監聽器在類級別宣告,其作用是為每個測試方法建立一個 Step 執行上下文,如下例所示

@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 框架,負責處理從配置的應用程式上下文進行依賴注入以注入 reader。另一個是 Spring Batch 的 StepScopeTestExecutionListener。它的工作原理是在測試用例中查詢 StepExecution 的工廠方法,並將其用作測試方法的上下文,就如同該執行在執行時在 Step 中處於活動狀態一樣。工廠方法透過其簽名(它必須返回一個 StepExecution)來檢測。如果未提供工廠方法,則會建立一個預設的 StepExecution

從 v4.1 開始,如果測試類使用 `@SpringBatchTest` 進行註解,則 `StepScopeTestExecutionListener` 和 `JobScopeTestExecutionListener` 會作為測試執行監聽器匯入。前面的測試示例可以配置如下

@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 範圍的持續時間與測試方法的執行時間一致,則監聽器方法很方便。對於更靈活但更具侵入性的方法,您可以使用 `StepScopeTestUtils`。以下示例計算前面示例中所示 reader 中可用專案的數量

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 元件的單元測試和整合測試時遇到的另一個常見問題是如何模擬領域物件。一個很好的例子是 `StepExecutionListener`,如下程式碼片段所示

public class NoWorkFoundStepExecutionListener implements StepExecutionListener {

    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` 需要 `JobExecution`,而 `JobExecution` 又需要 `JobInstance` 和 `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 中找到完整的方法列表。