單元測試
與其他應用程式風格一樣,對於作為批次處理 Job 一部分編寫的任何程式碼進行單元測試都極其重要。Spring 核心文件詳細介紹了如何使用 Spring 進行單元測試和整合測試,因此此處不再重複。然而,重要的是思考如何「端對端」測試批次處理 Job,這正是本章涵蓋的內容。spring-batch-test
專案包含的類別有助於這種端對端測試方法。
建立單元測試類別
為了讓單元測試執行批次處理 Job,框架必須載入 Job 的 ApplicationContext
。使用兩個註解來觸發此行為
-
@SpringJUnitConfig
表示該類別應使用 Spring 的 JUnit 功能 -
@SpringBatchTest
在測試上下文中注入 Spring Batch 測試工具 (例如JobLauncherTestUtils
和JobRepositoryTestUtils
)
如果測試上下文中包含單個 Job bean 定義,則此 bean 將在 JobLauncherTestUtils 中自動注入。否則,應在 JobLauncherTestUtils 上手動設定要測試的 job。 |
-
Java
-
XML
以下 Java 範例顯示了註解的用法
@SpringBatchTest
@SpringJUnitConfig(SkipSampleConfiguration.class)
public class SkipSampleFunctionalTests { ... }
以下 XML 範例顯示了註解的用法
@SpringBatchTest
@SpringJUnitConfig(locations = { "/simple-job-launcher-context.xml",
"/jobs/skipSampleJob.xml" })
public class SkipSampleFunctionalTests { ... }
批次處理 Job 的端對端測試
「端對端」測試可以定義為測試批次處理 Job 從開始到結束的完整執行。這允許進行設定測試條件、執行 Job 並驗證最終結果的測試。
考慮一個從資料庫讀取並寫入平面檔案的批次處理 Job 範例。測試方法首先設定包含測試資料的資料庫。它清除 CUSTOMER
表格,然後插入 10 筆新記錄。然後,測試使用 launchJob()
方法啟動 Job
。launchJob()
方法由 JobLauncherTestUtils
類別提供。JobLauncherTestUtils
類別也提供 launchJob(JobParameters)
方法,讓測試提供特定的參數。launchJob()
方法傳回 JobExecution
物件,這對於斷言有關 Job
執行的特定資訊非常有用。在以下情況中,測試驗證 Job
以 COMPLETED
狀態結束。
-
Java
-
XML
以下列表顯示了使用 JUnit 5 和 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 設定樣式的範例
@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
對於複雜的批次處理 Job,端對端測試方法中的測試案例可能會變得難以管理。在這些情況下,擁有測試案例來單獨測試個別 Step 可能會更有用。JobLauncherTestUtils
類別包含一個名為 launchStep
的方法,該方法採用 Step 名稱並僅執行該特定 Step
。這種方法允許進行更有針對性的測試,讓測試僅為該 Step 設定資料並直接驗證其結果。以下範例顯示如何使用 launchStep
方法按名稱載入 Step
。
JobExecution jobExecution = jobLauncherTestUtils.launchStep("loadFileStep");
測試 Step 作用域元件
通常,在運行時為 Step 設定的元件會使用 Step 作用域和延遲綁定,以便從 Step 或 Job 執行中注入上下文。這些元件很難作為獨立元件進行測試,除非您有一種方法可以設定上下文,使其如同它們在 Step 執行中一樣。這是 Spring Batch 中兩個元件的目標:StepScopeTestExecutionListener
和 StepScopeTestUtils
。
監聽器在類別層級宣告,其工作是為每個測試方法建立 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());
}
}
有兩個 TestExecutionListener
。一個是常規的 Spring Test 框架,它處理從設定的應用程式上下文到注入 reader 的依賴注入。另一個是 Spring Batch StepScopeTestExecutionListener
。它的工作方式是在測試案例中尋找 StepExecution
的 factory 方法,將其用作測試方法的上下文,如同該執行在運行時在 Step
中處於活動狀態一樣。factory 方法是透過其簽名偵測到的 (它必須傳回 StepExecution
)。如果未提供 factory 方法,則會建立預設的 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;
}
});
驗證輸出檔案
當批次處理 Job 寫入資料庫時,可以輕鬆查詢資料庫以驗證輸出是否符合預期。但是,如果批次處理 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));
模擬領域物件
在為 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
需要 JobExecution
,而 JobExecution
又需要 JobInstance
和 JobParameters
才能建立有效的 StepExecution
。雖然這在穩固的領域模型中很好,但它確實使為單元測試建立 Stub 物件變得冗長。為了解決此問題,Spring Batch 測試模組包含一個用於建立領域物件的 factory:MetaDataInstanceFactory
。有了這個 factory,單元測試可以更新為更簡潔,如下列範例所示。
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
的方法只是 factory 中可用的其中一種便利方法。您可以在其 Javadoc 中找到完整的 метод 列表。