kagamihogeの日記

kagamihogeの日記です。

Spring Batch 4.1.x - Reference Documentation - Unit Testingのテキトー翻訳

https://docs.spring.io/spring-batch/4.1.x/reference/html/testing.html#testing

https://qiita.com/kagamihoge/items/12fbbc2eac5b8a5ac1e0 俺の訳一覧リスト

1. Unit Testing

バッチ以外のアプリケーション同様に、バッチジョブの一部として書いたコードのユニットテストは極めて重要です。SpringのコアドキュメントにSpringでのユニットおよびインテグレーションテストの方法が良くまとめられているため、ここではそれについては触れません。ここでは、バッチジョブの'end to end'なテスト方法について解説します。spring-batch-testにはend-to-endのテスト手法を行うためのクラスがあります。

1.1. Creating a Unit Test Class

バッチジョブをユニットテストとして動作させるには、フレームワークでジョブのApplicationContextをロードする必要があります。これを行うための2つのアノテーションがあります。

v4.1から、@SpringBatchTestを使用するテストコンテキストでJobLauncherTestUtilsJobRepositoryTestUtilsなどSpring Batchのテストユーティリティをインジェクション可能になりました。

以下はアノテーションの使用例です。

Using Java Configuration

@SpringBatchTest
@RunWith(SpringRunner.class)
@ContextConfiguration(classes=SkipSampleConfiguration.class)
public class SkipSampleFunctionalTests { ... }

1.2. End-To-End Testing of Batch Jobs

'End To End'の定義はバッチジョブを最初から最後まで実行するテストです。テストコンディションのセットアップ・ジョブ実行・最終結果の検証をテストします。

以下はDBから読み込みフラットファイルに書き込むバッチジョブの例です。テストメソッドはまずテストデータをDBにセットアップします。CUSTOMERテーブルをクリアし、10レコードを新規追加します。それからテストはlaunchJob()Jobを起動します。launchJob()JobLauncherTestUtilsにあります。JobLauncherTestUtilsにはlaunchJob(JobParameters)もあり、テストにパラメータを指定できます。launchJob()JobExecutionを返し、Job実行に関する情報をassertするなどに使います。以下の例では、テストでJobが"COMPLETED"で終了することを検証しています。

Java Based Configuration

@SpringBatchTest
@RunWith(SpringRunner.class)
@ContextConfiguration(classes=SkipSampleConfiguration.class)
public class SkipSampleFunctionalTests {

    @Autowired
    private JobLauncherTestUtils jobLauncherTestUtils;

    private SimpleJdbcTemplate simpleJdbcTemplate;

    @Autowired
    public void setDataSource(DataSource dataSource) {
        this.simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource);
    }

    @Test
    public void testJob() throws Exception {
        simpleJdbcTemplate.update("delete from CUSTOMER");
        for (int i = 1; i <= 10; i++) {
            simpleJdbcTemplate.update("insert into CUSTOMER values (?, 0, ?, 100000)",
                                      i, "customer" + i);
        }

        JobExecution jobExecution = jobLauncherTestUtils.launchJob();


        Assert.assertEquals("COMPLETED", jobExecution.getExitStatus().getExitCode());
    }
}

1.3. Testing Individual Steps

複雑なバッチジョブでは、end-to-endのテストケースは扱い辛くなりがちです。この場合、stepごとにテストケースを作る方が良い場合があります。JobLauncherTestUtilsにはlaunchStepがあり、これはstep名でそのStepを実行します。これにより、特定step用のテストデータのセットアップをしてテスト実行をし、step実行結果を直接検証できます。以下はstep名でロードするのにlaunchStepを使用する例です。

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

1.4. Testing Step-Scoped Components

stepに設定するコンポーネントが実行時にstepスコープと遅延バインディングでstepやjobのコンテキストをインジェクトする場合があります。これをスタンドアローンコンポーネントとしてテストするのはトリッキーで、そのコンポーネントをstepに配置した状態を再現してコンテキストを設定する方法が必要です。このためにSpring BatchではStepScopeTestExecutionListenerStepScopeTestUtilsを用意しています。

このリスナーをクラスレベルに宣言すると、jobはstepコンテキストをテストメソッドごとに生成します。以下が例です。

@ContextConfiguration
@TestExecutionListeners( { DependencyInjectionTestExecutionListener.class,
    StepScopeTestExecutionListener.class })
@RunWith(SpringRunner.class)
public class StepScopeTestExecutionListenerIntegrationTests {

    // このコンポーネントはstepスコープで定義しており、stepがアクティブでないと
    // インジェクトできない。
    @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() {
        // readerを初期化して入力データをバインド
        assertNotNull(reader.read());
    }

}

ここには2つのTestExecutionListenersがあります。1つはSpring Testフレームワークのもので、設定したアプリケーションコンテキストからreaderのDIを処理するものです。もう1つはStepScopeTestExecutionListenerです。仕組みとしては、実行時にStepがアクティブかのように振る舞うように、StepExecutionのテストケースでファクトリメソッドを参照します。ファクトリメソッドはシグネチャStepExecutionを返す)で検出されます。ファクトリメソッドが無い場合、デフォルトのStepExecutionを生成します。

v4.1からは、@SpringBatchTestがある場合、StepScopeTestExecutionListenerJobScopeTestExecutionListenerをテストクラスにtest execution listenersとしてインポートします。上の例は以下のようにも書けます。

@SpringBatchTest
@RunWith(SpringRunner.class)
@ContextConfiguration
public class StepScopeTestExecutionListenerIntegrationTests {

    // このコンポーネントはstepスコープで定義しており、stepがアクティブでないと
    // インジェクトできない。
    @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() {
        // readerを初期化して入力データをバインド
        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;
    }
});

1.5. Validating Output Files

バッチジョブがDB書き込みする場合、その出力が期待通りか検証するのはDBにクエリを投げるだけです。しかし、バッチジョブがファイルに書き込む場合も、同じく、出力の検証は重要です。Spring Batchには出力ファイルの検証を行うAssertFileがあります。assertFileEqualsは2つのFile(もしくは2つのResource)を取り、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));

1.6. Mocking Domain Objects

Spring Batchのユニットおよびインテグレーションテストの作成でよくあるもう一つの課題はドメインオブジェクトのモックです。好例は、以下コードのような、StepExecutionListenerです。

public class NoWorkFoundStepExecutionListener extends StepExecutionListenerSupport {

    public ExitStatus afterStep(StepExecution stepExecution) {
        if (stepExecution.getReadCount() == 0) {
            return ExitStatus.FAILED;
        }
        return null;
    }
}

上のリスナーの例は、何も処理しなかったことを意味するreadカウントがゼロを、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のドメインモデルはオブジェクト指向に基づくため、StepExecutionJobExecutionを必要とし, JobExecutionは正しくStepExecutionを生成するためにJobParametersJobInstanceを必要とします。堅実なドメインモデルですが、ユニットテスト用のスタブオブジェクト生成は冗長です。このため、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にあります。