知識庫 / Spring RSS 訂閱

測試 Spring Batch 作業

Spring,Testing
HongKong
4
01:08 PM · Dec 06 ,2025

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-testspring-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 標註提供了 JobLauncherTestUtilsJobRepositoryTestUtils 輔助類。 我們使用它們來觸發測試中的 JobStep

我們的應用程序使用 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 方法來觸發特定的步驟。

請記住,我們還設計了 ItemReaderItemWriter 以在運行時使用動態值,這意味着我們可以將 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 領域對象進行樁化和模擬。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.