1. 引言
與基於 Spring 的其他應用程序不同,測試批處理作業存在一些特定的挑戰,主要源於作業執行的異步特性。
在本教程中,我們將探索測試 Spring Batch 作業的各種替代方案。
2. 所需依賴
我們使用了 spring-boot-starter-batch, 因此首先需要在我們的 pom.xml 中設置所需的依賴項:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-batch</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>3.3.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.batch</groupId>
<artifactId>spring-batch-test</artifactId>
<version>3.3.2.RELEASE</version>
<scope>test</scope>
</dependency>我們包含了 spring-boot-starter-test 和 spring-batch-test,它們引入了一些必要的輔助方法、監聽器和運行器,用於測試 Spring Batch 應用程序。
3. 定義 Spring Batch 作業
讓我們創建一個簡單的應用程序,演示 Spring Batch 如何解決一些測試挑戰。
我們的應用程序使用一個兩步 Job,它讀取包含結構化書籍信息的 CSV 輸入文件,並輸出書籍和書籍詳情。
3.1. 定義工作步驟
以下兩個步驟分別從 BookRecord 中提取特定信息,然後將其映射到 Book (步驟 1) 和 BookDetail (步驟 2):
@Bean
public Step step1(
ItemReader<BookRecord> csvItemReader, ItemWriter<Book> jsonItemWriter) throws IOException {
return stepBuilderFactory
.get("step1")
.<BookRecord, Book> chunk(3)
.reader(csvItemReader)
.processor(bookItemProcessor())
.writer(jsonItemWriter)
.build();
}
@Bean
public Step step2(
ItemReader<BookRecord> csvItemReader, ItemWriter<BookDetails> listItemWriter) {
return stepBuilderFactory
.get("step2")
.<BookRecord, BookDetails> chunk(3)
.reader(csvItemReader)
.processor(bookDetailsItemProcessor())
.writer(listItemWriter)
.build();
}3.2. 定義輸入讀取器和輸出寫入器
現在,讓我們使用 FlatFileItemReader 來將結構化的書籍信息反序列化為 BookRecord 對象,配置 CSV 文件輸入讀取器:
private static final String[] TOKENS = {
"bookname", "bookauthor", "bookformat", "isbn", "publishyear" };
@Bean
@StepScope
public FlatFileItemReader<BookRecord> csvItemReader(
@Value("#{jobParameters['file.input']}") String input) {
FlatFileItemReaderBuilder<BookRecord> builder = new FlatFileItemReaderBuilder<>();
FieldSetMapper<BookRecord> bookRecordFieldSetMapper = new BookRecordFieldSetMapper();
return builder
.name("bookRecordItemReader")
.resource(new FileSystemResource(input))
.delimited()
.names(TOKENS)
.fieldSetMapper(bookRecordFieldSetMapper)
.build();
}本定義中存在兩點重要事項,這將影響我們的測試方式。
首先,我們為 FlatItemReader Bean 註解了 @StepScope,因此該對象將與 StepExecution 共享其生命週期。
這還允許我們在運行時注入動態值,以便我們從 JobParameters 中傳遞輸入文件(如第4行所示)。 相比之下,BookRecordFieldSetMapper 中使用的令牌在編譯時進行配置。
然後,我們同樣定義 JsonFileItemWriter 輸出寫入器:
@Bean
@StepScope
public JsonFileItemWriter<Book> jsonItemWriter(
@Value("#{jobParameters['file.output']}") String output) throws IOException {
JsonFileItemWriterBuilder<Book> builder = new JsonFileItemWriterBuilder<>();
JacksonJsonObjectMarshaller<Book> marshaller = new JacksonJsonObjectMarshaller<>();
return builder
.name("bookItemWriter")
.jsonObjectMarshaller(marshaller)
.resource(new FileSystemResource(output))
.build();
}
第二步,我們使用 Spring Batch 提供的 ListItemWriter,它將數據直接寫入內存列表。
3.3. 定義自定義 JobLauncher
接下來,我們通過在我們的 application.properties 中設置 spring.batch.job.enabled=false 來禁用 Spring Boot Batch 的默認 Job 啓動配置。
我們配置自己的 JobLauncher,以便在啓動 Job 時傳遞自定義的 JobParameters 實例:
@SpringBootApplication
public class SpringBatchApplication implements CommandLineRunner {
// autowired jobLauncher and transformBooksRecordsJob
@Value("${file.input}")
private String input;
@Value("${file.output}")
private String output;
@Override
public void run(String... args) throws Exception {
JobParametersBuilder paramsBuilder = new JobParametersBuilder();
paramsBuilder.addString("file.input", input);
paramsBuilder.addString("file.output", output);
jobLauncher.run(transformBooksRecordsJob, paramsBuilder.toJobParameters());
}
// other methods (main etc.)
}
4. 測試 Spring Batch 任務
<em >spring-batch-test</em> 依賴項提供了一組有用的輔助方法和監聽器,可用於在測試期間配置 Spring Batch 上下文。
讓我們創建一個基本的測試結構:
@RunWith(SpringRunner.class)
@SpringBatchTest
@EnableAutoConfiguration
@ContextConfiguration(classes = { SpringBatchConfiguration.class })
@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class,
DirtiesContextTestExecutionListener.class})
@DirtiesContext(classMode = ClassMode.AFTER_CLASS)
public class SpringBatchIntegrationTest {
// other test constants
@Autowired
private JobLauncherTestUtils jobLauncherTestUtils;
@Autowired
private JobRepositoryTestUtils jobRepositoryTestUtils;
@After
public void cleanUp() {
jobRepositoryTestUtils.removeJobExecutions();
}
private JobParameters defaultJobParameters() {
JobParametersBuilder paramsBuilder = new JobParametersBuilder();
paramsBuilder.addString("file.input", TEST_INPUT);
paramsBuilder.addString("file.output", TEST_OUTPUT);
return paramsBuilder.toJobParameters();
}
The @SpringBatchTest 標註提供了 JobLauncherTestUtils 和 JobRepositoryTestUtils 輔助類。 我們使用它們來觸發測試中的 Job 和 Step。
我們的應用程序使用 Spring Boot 自定義配置,這使得默認的內存 JobRepository 能夠啓用。 結果是,在同一類中運行多個測試需要每個測試運行後執行清理步驟。
最後,如果我們想從多個測試類中運行多個測試,則需要標記我們的上下文為髒污。 這對於避免多個 JobRepository 實例使用相同的數據庫源而產生的衝突是必需的。
4.1. 測試端到端 作業
首先,我們將測試一個包含小數據集的完整端到端 作業。
然後,我們可以將結果與預期測試輸出進行比較。
@Test
public void givenReferenceOutput_whenJobExecuted_thenSuccess() throws Exception {
// given
FileSystemResource expectedResult = new FileSystemResource(EXPECTED_OUTPUT);
FileSystemResource actualResult = new FileSystemResource(TEST_OUTPUT);
// when
JobExecution jobExecution = jobLauncherTestUtils.launchJob(defaultJobParameters());
JobInstance actualJobInstance = jobExecution.getJobInstance();
ExitStatus actualJobExitStatus = jobExecution.getExitStatus();
// then
assertThat(actualJobInstance.getJobName(), is("transformBooksRecords"));
assertThat(actualJobExitStatus.getExitCode(), is("COMPLETED"));
AssertFile.assertFileEquals(expectedResult, actualResult);
}Spring Batch Test 提供了一種有用的 文件比較方法,用於使用 AssertFile 類驗證輸出。
4.2. 測試單個步驟
有時測試完整的 作業 端到端成本很高,因此測試單個 步驟 更明智:
@Test
public void givenReferenceOutput_whenStep1Executed_thenSuccess() throws Exception {
// given
FileSystemResource expectedResult = new FileSystemResource(EXPECTED_OUTPUT);
FileSystemResource actualResult = new FileSystemResource(TEST_OUTPUT);
// when
JobExecution jobExecution = jobLauncherTestUtils.launchStep(
"step1", defaultJobParameters());
Collection actualStepExecutions = jobExecution.getStepExecutions();
ExitStatus actualJobExitStatus = jobExecution.getExitStatus();
// then
assertThat(actualStepExecutions.size(), is(1));
assertThat(actualJobExitStatus.getExitCode(), is("COMPLETED"));
AssertFile.assertFileEquals(expectedResult, actualResult);
}
@Test
public void whenStep2Executed_thenSuccess() {
// when
JobExecution jobExecution = jobLauncherTestUtils.launchStep(
"step2", defaultJobParameters());
Collection actualStepExecutions = jobExecution.getStepExecutions();
ExitStatus actualExitStatus = jobExecution.getExitStatus();
// then
assertThat(actualStepExecutions.size(), is(1));
assertThat(actualExitStatus.getExitCode(), is("COMPLETED"));
actualStepExecutions.forEach(stepExecution -> {
assertThat(stepExecution.getWriteCount(), is(8));
});
}請注意,我們使用 launchStep 方法來觸發特定的步驟。
請記住,我們還設計了 ItemReader 和 ItemWriter 以在運行時使用動態值,這意味着我們可以將 I/O 參數傳遞給 JobExecution (第9行和第23行).
對於第一個 Step 測試,我們比較實際輸出與預期輸出。
另一方面,在第二個測試中,我們驗證了 StepExecution,以驗證寫入的預期項目。
4.3. 測試範圍組件
現在我們來測試 FlatFileItemReader。 回顧一下,我們將其暴露為 @StepScope Bean,因此我們將使用 Spring Batch 專門為此提供支持:
// previously autowired itemReader
@Test
public void givenMockedStep_whenReaderCalled_thenSuccess() throws Exception {
// given
StepExecution stepExecution = MetaDataInstanceFactory
.createStepExecution(defaultJobParameters());
// when
StepScopeTestUtils.doInStepScope(stepExecution, () -> {
BookRecord bookRecord;
itemReader.open(stepExecution.getExecutionContext());
while ((bookRecord = itemReader.read()) != null) {
// then
assertThat(bookRecord.getBookName(), is("Foundation"));
assertThat(bookRecord.getBookAuthor(), is("Asimov I."));
assertThat(bookRecord.getBookISBN(), is("ISBN 12839"));
assertThat(bookRecord.getBookFormat(), is("hardcover"));
assertThat(bookRecord.getPublishingYear(), is("2018"));
}
itemReader.close();
return null;
});
}MetadataInstanceFactory 創建一個自定義的 StepExecution,用於注入我們的 Step 級 ItemReader。
因此,我們可以藉助 doInTestScope 方法來檢查讀取器的行為。
接下來,讓我們測試 JsonFileItemWriter 並驗證其輸出:
@Test
public void givenMockedStep_whenWriterCalled_thenSuccess() throws Exception {
// given
FileSystemResource expectedResult = new FileSystemResource(EXPECTED_OUTPUT_ONE);
FileSystemResource actualResult = new FileSystemResource(TEST_OUTPUT);
Book demoBook = new Book();
demoBook.setAuthor("Grisham J.");
demoBook.setName("The Firm");
StepExecution stepExecution = MetaDataInstanceFactory
.createStepExecution(defaultJobParameters());
// when
StepScopeTestUtils.doInStepScope(stepExecution, () -> {
jsonItemWriter.open(stepExecution.getExecutionContext());
jsonItemWriter.write(Arrays.asList(demoBook));
jsonItemWriter.close();
return null;
});
// then
AssertFile.assertFileEquals(expectedResult, actualResult);
}
與之前的測試不同,我們現在完全掌控測試對象。因此,我們負責打開和關閉 I/O 流。
5. 結論
在本教程中,我們探討了測試 Spring Batch 任務的各種方法。
端到端測試驗證任務的完整執行。在複雜場景中,測試單個步驟可能會有所幫助。
最後,對於步驟級別的組件,我們可以使用 <em >spring-batch-test</em> 提供的各種輔助方法。這些方法將幫助我們對 Spring Batch 領域對象進行樁化和模擬。