1. 序章

他のSpringベースのアプリケーションとは異なり、バッチジョブのテストには、主にジョブの実行方法の非同期性が原因で、いくつかの特定の課題が伴います。

このチュートリアルでは、Springバッチジョブをテストするためのさまざまな代替案を検討します。

2. 必要な依存関係

spring -boot-starter-batch を使用しているので、最初にpom.xmlで必要な依存関係を設定しましょう。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-batch</artifactId>
    <version>2.6.1</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <version>2.6.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.batch</groupId>
    <artifactId>spring-batch-test</artifactId>
    <version>4.3.0.RELEASE</version>
    <scope>test</scope>
</dependency>

Spring-boot-starter-testとspring-batch-testを含めました。これらは、Spring Batchアプリケーションをテストするために必要なヘルパーメソッド、リスナー、ランナーをいくつか取り入れています。

3. SpringBatchジョブの定義

SpringBatchがテストの課題のいくつかをどのように解決するかを示す簡単なアプリケーションを作成しましょう。

このアプリケーションは、構造化された本の情報を含むCSV入力ファイルを読み取り、本と本の詳細を出力する2段階のJobを使用します。

3.1. ジョブステップの定義

後続の2つのStepは、 BookRecord から特定の情報を抽出し、これらを Book (step1)および BookDetail (step2)にマップします。 )::

@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 を使用してCSVファイル入力リーダーを構成し、構造化された本の情報をBookRecordオブジェクトに逆シリアル化します。

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

この定義にはいくつかの重要なことがあり、それは私たちがテストする方法に影響を及ぼします。

まず、FlatItemReaderBeanに @StepScope、の注釈を付けた結果、このオブジェクトはその存続期間をStepExecutionと共有します。[X171X ]

これにより、実行時に動的な値を挿入できるため、4行目のJobParametersから入力ファイルを渡すことができます。 対照的に、 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();
}

2番目のStepには、SpringBatchが提供するListItemWriter を使用します。これは、メモリ内のリストにデータをダンプするだけです。

3.3. カスタムJobLauncherの定義

次に、application.properties。spring .batch.job.enabled = false を設定して、Spring BootバッチのデフォルトのJob起動構成を無効にします。 ]

Job を起動するときに、カスタム JobParameters インスタンスを渡すように、独自のJobLauncherを構成します。

@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. SpringBatchジョブのテスト

spring-batch-test 依存関係は、テスト中にSpringBatchコンテキストを構成するために使用できる一連の便利なヘルパーメソッドとリスナーを提供します。

テストの基本構造を作成しましょう。

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

@SpringBatchTestアノテーションは、JobLauncherTestUtilsおよびJobRepositoryTestUtilsヘルパークラスを提供します。これらを使用して、テストでJobおよびStepをトリガーします。

このアプリケーションでは、 Spring Boot自動構成を使用して、デフォルトのメモリ内JobRepositoryを有効にします。その結果、同じクラスで複数のテストを実行するには、各テスト実行後にクリーンアップ手順が必要です

最後に、複数のテストクラスから複数のテストを実行する場合は、コンテキストをdirtyとしてマークする必要があります。 これは、同じデータソースを使用する複数のJobRepositoryインスタンスの衝突を回避するために必要です。

4.1. エンドツーエンドのジョブのテスト

最初にテストするのは、小さなデータセット入力を使用した完全なエンドツーエンドのJobです。

次に、結果を予想されるテスト出力と比較できます。

@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テストでは、実際の出力を期待される出力と比較します。

一方、2番目のテストでは、予想される書き込みアイテムのStepExecutionを検証します。

4.3. ステップスコープのコンポーネントのテスト

それでは、テストしてみましょう FlatFileItemReader @StepScope beanとして公開したことを思い出してください。そのため、SpringBatchの専用サポートを使用する必要があります。

// 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 は、ステップスコープのItemReader。を挿入するために必要なカスタムStepExecutionを作成します。

このため、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バッチジョブをテストするさまざまなアプローチについて説明しました。

エンドツーエンドのテストは、ジョブの完全な実行を検証します。 個々のステップをテストすると、複雑なシナリオで役立つ場合があります。

最後に、ステップスコープのコンポーネントに関しては、 spring-batch-testによって提供される一連のヘルパーメソッドを使用できます。これらは、SpringBatchドメインオブジェクトのスタブとモックを作成するのに役立ちます。

いつものように、GitHubで完全なコードベースを調べることができます。