Job全体を起動して確認する

Spring Batchを使うと、CSV取込、データ移行、夜間バッチ、集計処理などの定期処理をJavaで実装できます。

ただ、Spring Batchの処理は最初は「どうやってテストすればいいのか」が分かりづらいです。

この記事では、Spring Batchの簡単なJobを作成し、そのJobをテストコードから起動して、正常終了することを確認する方法を紹介します。

今回確認する内容は次の通りです。

  • Spring Batchに必要な依存関係
  • 簡単なJobとStepの実装
  • Repositoryを呼び出すBatch処理
  • @SpringBatchTestを使ったテスト
  • JobLauncherTestUtilsを使ったJob全体の起動
  • ExitStatus.COMPLETEDによる正常終了の確認

Spring Batchのテストで確認すること

Spring Batchでは、処理全体をJobとして定義します。 そして、Jobの中で実行する1つ1つの処理をStepとして定義します。

Spring BatchのテストではテストコードからJobを起動します。 そして、Jobの実行結果であるJobExecutionを受け取り、正常終了したかどうかを確認します。

今回のテストでは、Jobの終了状態を表すExitStatusを確認します。

assertThat(jobExecution.getExitStatus())
        .isEqualTo(ExitStatus.COMPLETED);

ExitStatus.COMPLETEDになっていれば、Jobが最後まで正常に完了したと判断できます。

依存関係を追加する

まず、Spring Batchを使うためにbuild.gradleへ依存関係を追加します。

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-batch'
    implementation 'org.springframework.boot:spring-boot-starter-jdbc'

    runtimeOnly 'com.h2database:h2'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.batch:spring-batch-test'
}

それぞれの役割は次の通りです。

  • spring-boot-starter-batch:Spring BootでSpring Batchを使うための依存関係
  • spring-boot-starter-jdbc:DB登録処理でJdbcTemplateを使うための依存関係
  • h2:テスト用のインメモリDB
  • spring-boot-starter-test:JUnitやAssertJなど、テストに必要な機能
  • spring-batch-test:Spring Batchのテスト用ライブラリ

注意点として、spring-batch-testはSpring Boot側ではなくSpring Batch側のライブラリです。 そのため、依存関係の書き方は次のようになります。

testImplementation 'org.springframework.batch:spring-batch-test'

テスト用の設定ファイルを作成する

次に、src/test/resources/application-test.ymlを作成します。

spring:
  datasource:
    url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1
    driver-class-name: org.h2.Driver
    username: sa
    password:

  batch:
    jdbc:
      initialize-schema: always

spring.batch.jdbc.initialize-schema=alwaysを指定することで、Spring Batchが内部で使うメタテーブルをテスト実行時に作成できます。

Spring Batchは、Jobの実行状態やStepの実行状態を管理するために、内部的にメタテーブルを使用します。 H2のようなインメモリDBでテストする場合でも、このテーブルが必要になります。

今回作るBatch処理

今回は、サンプルとして次のような簡単なBatch処理を作ります。

orderImportJobを起動する
  ↓
orderImportStepが実行される
  ↓
OrderRepositoryのsaveメソッドを呼び出す
  ↓
ORDER_INFOテーブルにデータを登録する

複雑なCSV取込やファイル出力ではなく、まずはSpring Batchのテスト方法を理解するために、Repositoryを1回呼び出すだけのシンプルな構成にします。

テスト用のテーブルを作成する

今回は、Batch処理の中でORDER_INFOテーブルにデータを登録します。 そのため、src/test/resources/schema.sqlを作成します。

CREATE TABLE ORDER_INFO (
    ID BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    ITEM_NAME VARCHAR(255) NOT NULL,
    QUANTITY INT NOT NULL
);

このテーブルに、Batch処理から商品名と数量を登録します。

Repositoryを作成する

次に、DBへ登録するためのRepositoryを作成します。

@Repository
public class OrderRepository {

    private final JdbcTemplate jdbcTemplate;

    public OrderRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public void save(String itemName, int quantity) {
        jdbcTemplate.update(
                "INSERT INTO ORDER_INFO (ITEM_NAME, QUANTITY) VALUES (?, ?)",
                itemName,
                quantity
        );
    }

    public int count() {
        Integer count = jdbcTemplate.queryForObject(
                "SELECT COUNT(*) FROM ORDER_INFO",
                Integer.class
        );

        return count == null ? 0 : count;
    }

    public void deleteAll() {
        jdbcTemplate.update("DELETE FROM ORDER_INFO");
    }
}

saveメソッドでは、ORDER_INFOテーブルにデータを登録しています。

countメソッドは、テストで登録件数を確認するために用意しています。 deleteAllメソッドは、テスト実行前にテーブルを空にするために使います。

Spring BatchのJobとStepを実装する

次に、Spring BatchのJobとStepを定義します。

@Configuration
public class OrderImportJobConfig {

    @Bean
    Job orderImportJob(
            JobRepository jobRepository,
            Step orderImportStep
    ) {
        return new JobBuilder("orderImportJob", jobRepository)
                .start(orderImportStep)
                .build();
    }

    @Bean
    Step orderImportStep(
            JobRepository jobRepository,
            PlatformTransactionManager transactionManager,
            OrderRepository orderRepository
    ) {
        return new StepBuilder("orderImportStep", jobRepository)
                .tasklet((contribution, chunkContext) -> {
                    orderRepository.save("ノートPC", 2);
                    return RepeatStatus.FINISHED;
                }, transactionManager)
                .build();
    }
}

このクラスには@Configurationを付けています。 @Configurationを付けることで、このクラスがSpringに設定クラスとして読み込まれます。

orderImportJobメソッドでは、Jobを定義しています。

new JobBuilder("orderImportJob", jobRepository)
        .start(orderImportStep)
        .build();

ここでは、orderImportJobという名前のJobを作成し、起動時にorderImportStepを実行するようにしています。

次に、orderImportStepメソッドではStepを定義しています。

new StepBuilder("orderImportStep", jobRepository)
        .tasklet((contribution, chunkContext) -> {
            orderRepository.save("ノートPC", 2);
            return RepeatStatus.FINISHED;
        }, transactionManager)
        .build();

taskletの中に、Stepで実行したい処理を書きます。 今回は、OrderRepositorysaveメソッドを呼び出して、DBにデータを登録しています。

最後にRepeatStatus.FINISHEDを返すことで、このStepの処理が完了したことを表します。

Job全体をテストする

では、実際にJob全体を起動するテストコードを書いていきます。

@SpringBatchTest
@SpringBootTest(properties = "spring.batch.job.enabled=false")
@ActiveProfiles("test")
class OrderImportJobTest {

    @Autowired
    private JobLauncherTestUtils jobLauncherTestUtils;

    @Autowired
    private OrderRepository orderRepository;

    @Test
    void job全体を起動して正常終了しデータが登録されること() throws Exception {
        orderRepository.deleteAll();

        JobParameters jobParameters = new JobParametersBuilder()
                .addLong("run.id", System.currentTimeMillis())
                .toJobParameters();

        JobExecution jobExecution = jobLauncherTestUtils.launchJob(jobParameters);

        assertThat(jobExecution.getExitStatus())
                .isEqualTo(ExitStatus.COMPLETED);

        assertThat(orderRepository.count())
                .isEqualTo(1);
    }
}

このテストでは、次の流れでJobを確認しています。

テーブルを空にする
  ↓
JobParametersを作成する
  ↓
JobLauncherTestUtilsでJobを起動する
  ↓
JobExecutionを受け取る
  ↓
ExitStatusがCOMPLETEDであることを確認する
  ↓
DBに1件登録されていることを確認する

@SpringBatchTestとは

@SpringBatchTestは、Spring Batchのテスト用機能を使うためのアノテーションです。

このアノテーションを付けることで、テストコードからJobやStepを起動するための補助クラスを使えるようになります。 今回使っているJobLauncherTestUtilsも、Spring BatchのテストでJobを起動するためのクラスです。

@Autowired
private JobLauncherTestUtils jobLauncherTestUtils;

JobLauncherTestUtilsを使うと、次のようにテストコードからJob全体を起動できます。

JobExecution jobExecution = jobLauncherTestUtils.launchJob(jobParameters);

@SpringBootTestを付ける理由

今回のテストでは@SpringBootTestも付けています。

@SpringBootTest(properties = "spring.batch.job.enabled=false")

@SpringBootTestを付けることで、Springに登録されているBeanを読み込んだ状態でテストできます。

今回のBatch処理では、次のようなBeanを使っています。

  • Job
  • Step
  • OrderRepository
  • JdbcTemplate
  • JobRepository
  • PlatformTransactionManager

これらをSpringに読み込ませた状態でJobを起動したいため、@SpringBootTestを使っています。

spring.batch.job.enabled=falseを指定する理由

テストコードでは、次のように指定しています。

@SpringBootTest(properties = "spring.batch.job.enabled=false")

Spring BootでSpring BatchのJobが登録されている場合、アプリケーション起動時にJobが自動実行されることがあります。

ただし、今回のテストでやりたいのは、Spring Bootが起動したタイミングでJobを動かすことではありません。 テストメソッドの中で、次のコードを呼び出したタイミングでJobを起動したいです。

jobLauncherTestUtils.launchJob(jobParameters);

そのため、spring.batch.job.enabled=falseを指定して、起動時の自動実行を止めています。

これを指定しないと、テスト開始時にJobが先に動いてしまい、テストメソッド内で起動したJobと合わせて二重実行のような状態になる可能性があります。

JobParametersを指定する理由

Jobを起動するときには、JobParametersを渡しています。

JobParameters jobParameters = new JobParametersBuilder()
        .addLong("run.id", System.currentTimeMillis())
        .toJobParameters();

Spring Batchでは、同じJobを同じJobParametersで実行すると、同じJob実行として扱われます。

そのため、一度正常終了したJobを同じパラメータでもう一度実行しようとすると、すでに完了済みとして扱われることがあります。

テストは何度も実行することが多いため、毎回異なるJobParametersを渡した方が扱いやすいです。

今回は、run.idに現在時刻を入れることで、テスト実行ごとに別のJob実行として扱えるようにしています。

.addLong("run.id", System.currentTimeMillis())

ExitStatus.COMPLETEDで正常終了を確認する

Jobを起動すると、戻り値としてJobExecutionが返ってきます。

JobExecution jobExecution = jobLauncherTestUtils.launchJob(jobParameters);

JobExecutionには、Jobの実行結果に関する情報が入っています。 その中からgetExitStatus()を使って終了状態を取得します。

assertThat(jobExecution.getExitStatus())
        .isEqualTo(ExitStatus.COMPLETED);

ExitStatus.COMPLETEDであれば、Jobが最後まで正常に完了したことを確認できます。

まずは、この確認がSpring BatchのJobテストの基本になります。

DB登録結果も確認する

Jobが正常終了しただけでは、「本当に期待した処理が行われたか」までは分かりません。

今回のJobでは、Stepの中でDB登録を行っています。 そのため、Jobの正常終了に加えて、DBに1件登録されていることも確認しています。

assertThat(orderRepository.count())
        .isEqualTo(1);

このように、Spring Batchのテストでは次の2つを組み合わせると分かりやすいです。

Jobが正常終了したか
処理結果が期待通りか

ExitStatus.COMPLETEDでJobの終了状態を確認し、DBの件数確認で処理結果を確認する、という形です。

複数のJobがある場合の注意点

今回のサンプルでは、テスト対象のJobが1つだけという前提です。

もしアプリケーション内に複数のJobがある場合は、JobLauncherTestUtilsがどのJobを起動すればよいか分からなくなることがあります。

その場合は、テスト対象のJobを明示的に指定します。

@Autowired
private JobLauncherTestUtils jobLauncherTestUtils;

@Autowired
@Qualifier("orderImportJob")
private Job orderImportJob;

@BeforeEach
void setUp() {
    jobLauncherTestUtils.setJob(orderImportJob);
}

複数Jobがあるプロジェクトでは、どのJobをテストするのかを明示しておくと安全です。

まとめ

この記事では、Spring BatchのJob全体をテストする方法を紹介しました。

Spring Batchのテストでは、APIテストのようにURLを呼び出すのではなく、テストコードからJobを起動します。

基本の流れは次の通りです。

@SpringBatchTestを付ける
@SpringBootTestでSpringのBeanを読み込む
spring.batch.job.enabled=falseで起動時の自動実行を止める
JobParametersを作成する
JobLauncherTestUtilsでJobを起動する
JobExecutionのExitStatusを確認する
必要に応じてDB登録結果も確認する

まずは、ExitStatus.COMPLETEDを確認できれば、Jobが最後まで正常終了したことをテストできます。

さらに、DB登録件数なども確認すると、Batch処理の結果までテストできます。

Spring Batchのテストでは、Job全体を起動して確認する方法を押さえておくと、CSV取込、データ移行、集計処理など、さまざまなBatch処理のテストに応用できます。