ユニットテスト

他のアプリケーションスタイルと同様に、バッチジョブの一部として記述されたコードを単体テストすることは非常に重要です。Spring のコアドキュメントには、Spring を使用した単体テストと統合テストの方法が詳細に記載されているため、ここでは繰り返しません。ただし、バッチジョブを「エンドツーエンド」でテストする方法について考えることが重要です。これについては、この章で説明します。spring-batch-test プロジェクトには、このエンドツーエンドのテストアプローチを容易にするクラスが含まれています。

単体テストクラスの作成

単体テストでバッチジョブを実行するには、フレームワークがジョブの ApplicationContext を読み込む必要があります。この動作をトリガーするために、2 つのアノテーションが使用されます。

  • @SpringJUnitConfig は、クラスが Spring の JUnit 機能を使用する必要があることを示します

  • @SpringBatchTest は、テストコンテキストに Spring Batch テストユーティリティ ( JobLauncherTestUtils や JobRepositoryTestUtils など) を挿入します。

テストコンテキストに単一の 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() メソッドを使用して Job を起動します。launchJob() メソッドは、JobLauncherTestUtils クラスによって提供されます。JobLauncherTestUtils クラスは、テストで特定のパラメーターを指定できるようにする launchJob(JobParameters) メソッドも提供します。launchJob() メソッドは、Job 実行に関する特定の情報をアサートするのに役立つ JobExecution オブジェクトを返します。次の場合、テストは Job が COMPLETED のステータスで終了したことを検証します。

  • Java

  • XML

次のリストは、Java 構成スタイルの 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());
    }
}

次のリストは、XML 構成スタイルの 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());
    }
}

個々のステップのテスト

複雑なバッチジョブの場合、エンドツーエンドのテストアプローチのテストケースは管理不能になる可能性があります。これらの場合、個々のステップを単独でテストするためのテストケースを用意する方が便利です。JobLauncherTestUtils クラスには、launchStep と呼ばれるメソッドが含まれています。このメソッドは、ステップ名を取り、その特定の Step のみを実行します。このアプローチにより、よりターゲットを絞ったテストが可能になり、テストはそのステップのみのデータを設定し、結果を直接検証できます。次の例は、launchStep メソッドを使用して、名前で Step をロードする方法を示しています。

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

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

多くの場合、実行時にステップ用に構成されたコンポーネントは、ステップスコープと遅延バインディングを使用して、ステップまたはジョブの実行からコンテキストを注入します。コンテキストをステップ実行のように設定する方法がない限り、これらはスタンドアロンコンポーネントとしてテストするのが困難です。それが、Spring Batch の 2 つのコンポーネント StepScopeTestExecutionListener と StepScopeTestUtils のゴールです。

リスナーはクラスレベルで宣言され、そのロールは、次の例に示すように、各テストメソッドのステップ実行コンテキストを作成することです。

@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 が 2 つあります。1 つは通常の Spring Test フレームワークで、構成されたアプリケーションコンテキストからの依存性注入を処理してリーダーを注入します。もう 1 つは Spring Batch StepScopeTestExecutionListener です。実行時に Step で実行がアクティブであるかのように、それをテストメソッドのコンテキストとして使用して、StepExecution のテストケースでファクトリメソッドを探すことによって機能します。ファクトリメソッドはその署名によって検出されます (それは StepExecution を返す必要があります)。ファクトリメソッドが指定されていない場合は、デフォルトの StepExecution が作成されます。

v4.1 から開始して、StepScopeTestExecutionListener および JobScopeTestExecutionListener は、テストクラスに @SpringBatchTest アノテーションが付けられている場合、テスト実行リスナーとしてインポートされます。上記のテスト例は、次のように構成できます。

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

}

リスナーアプローチは、ステップスコープの期間をテストメソッドの実行にする場合に便利です。より柔軟であるがより侵襲的なアプローチのために、StepScopeTestUtils を使用できます。次の例では、前の例で示したリーダーで使用可能なアイテムの数をカウントします。

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 は、出力ファイルの検証を容易にするために AssertFile と呼ばれるクラスを提供します。assertFileEquals と呼ばれるメソッドは、2 つの File オブジェクト (または 2 つの Resource オブジェクト) を取り、2 つのファイルが同じ内容を持っていることを 1 行ずつアサートします。次の例に示すように、期待される出力でファイルを作成し、それを実際の結果と比較することができます。

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 コンポーネントの単体テストと統合テストを作成するときに発生するもう 1 つの一般的な課題は、ドメインオブジェクトをモックする方法です。次のコードスニペットが示すように、良い例は 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 を作成するには、StepExecution には JobInstance と JobParameters を必要とする JobExecution が必要です。これは堅実なドメインモデルでは有効ですが、単体テスト用のスタブオブジェクトの作成が冗長になります。この課題に対処するために、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 を作成する前述のメソッドは、ファクトリ内で使用できる便利なメソッドの 1 つにすぎません。Javadoc で完全なメソッドのリストを見つけることができます。