【エンジニア向け】SpringBatchでテストを自動化しよう

今回は、SpringBatchプロジェクトにおけるテスト自動化を試してみたいと思います。

前提として、SpringBootを用いたプロジェクト作成については、以下の記事で解説しているので、Spring自体が初めての方は、こちらを先に勉強してもらえればと思います。

【エンジニア向け】SpringBootでWebアプリのプロジェクトを作ろう

SpringBatchでテストを自動化しよう

  1. アーキテクチャの確認
  2. Spring Bootプロジェクトの作成
  3. Spring Batchのサンプルコード
  4. Spring Batchのテスト自動化

1. アーキテクチャの確認

SpringBatchによるテスト自動化する際のアーキテクチャは、以下の組み合わせで構成します。

アーキテクチャ選定技術/バージョン
Javaのバージョンjdk11
ビルドタイプGradle
DBPostgres12
SpringBootorg.springframework.boot.2.4.5
SpringBatchorg.springframework.boot:spring-boot-starter-batch
io.spring.dependency-management1.0.11.RELEASE

2. Spring Bootプロジェクトの作成

次にSpringBootのベースプロジェクトを用意しましょう。初めての方は以下の記事から作成を試してみてください。

【エンジニア向け】SpringBootでWebアプリのプロジェクトを作ろう

3. Spring Batchのサンプルコード

公式サイトを参考に、SpringBatchのサンプルコードを模写しましょう。

package com.example.batchprocessing;

import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.item.database.BeanPropertyItemSqlParameterSourceProvider;
import org.springframework.batch.item.database.JdbcBatchItemWriter;
import org.springframework.batch.item.database.builder.JdbcBatchItemWriterBuilder;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.builder.FlatFileItemReaderBuilder;
import org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;

import javax.sql.DataSource;

@Configuration
@EnableBatchProcessing
public class BatchConfiguration {

    @Autowired
    public JobBuilderFactory jobBuilderFactory;

    @Autowired
    public StepBuilderFactory stepBuilderFactory;

    @Bean
    public FlatFileItemReader<Person> reader() {
        return new FlatFileItemReaderBuilder<Person>()
                .name("personItemReader")
                .resource(new ClassPathResource("sample-data.csv"))
                .delimited()
                .names(new String[]{"firstName", "lastName"})
                .fieldSetMapper(new BeanWrapperFieldSetMapper<Person>() {{
                    setTargetType(Person.class);
                }})
                .build();
    }

    @Bean
    public PersonItemProcessor processor() {
        return new PersonItemProcessor();
    }

    @Bean
    public JdbcBatchItemWriter<Person> writer(DataSource dataSource) {
        return new JdbcBatchItemWriterBuilder<Person>()
                .itemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<>())
                .sql("INSERT INTO people (first_name, last_name) VALUES (:firstName, :lastName)")
                .dataSource(dataSource)
                .build();
    }

    @Bean
    public Job importUserJob(JobCompletionNotificationListener listener, Step step1) {
        return jobBuilderFactory.get("importUserJob")
                .incrementer(new RunIdIncrementer())
                .listener(listener)
                .flow(step1)
                .end()
                .build();
    }

    @Bean
    public Step step1(JdbcBatchItemWriter<Person> writer) {
        return stepBuilderFactory.get("step1")
                .<Person, Person> chunk(10)
                .reader(reader())
                .processor(processor())
                .writer(writer)
                .build();
    }
}

JOB、STEP、READER、Processor、Writerの役割や使い方は、公式サイトに説明があるためそちらをご覧ください。

上記のBatchConfiguration.javaでは、SpringBatchのバッチの設定を記載しています。

昔はXMLのみで設定可能だったものが、今はJavaファイルで記載可能となってます。

同様に、公式サイトを参考に以下のファイルも模写します。

下記はSpring Batchには直接関係ないですが、BatchのProcesserでシンプルな処理を確認するためのクラスとなります。

package com.example.batchprocessing;

public class Person {

    private String lastName;
    private String firstName;

    public Person() {
    }

    public Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    @Override
    public String toString() {
        return "firstName: " + firstName + ", lastName: " + lastName;
    }

}
package com.example.batchprocessing;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.batch.item.ItemProcessor;

public class PersonItemProcessor implements ItemProcessor<Person, Person> {

    private static final Logger log = LoggerFactory.getLogger(PersonItemProcessor.class);

    @Override
    public Person process(final Person person) throws Exception {
        final String firstName = person.getFirstName().toUpperCase();
        final String lastName = person.getLastName().toUpperCase();

        final Person transformedPerson = new Person(firstName, lastName);

        log.info("Converting (" + person + ") into (" + transformedPerson + ")");

        return transformedPerson;
    }

}

次にListnerを追加します、これによりジョブの実施後、実施前などに共通して処理を入れたいときに役に立ちます。

package com.example.batchprocessing;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.BatchStatus;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.listener.JobExecutionListenerSupport;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;

@Component
public class JobCompletionNotificationListener extends JobExecutionListenerSupport {

    private static final Logger log = LoggerFactory.getLogger(JobCompletionNotificationListener.class);

    private final JdbcTemplate jdbcTemplate;

    @Autowired
    public JobCompletionNotificationListener(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    @Override
    public void afterJob(JobExecution jobExecution) {
        if(jobExecution.getStatus() == BatchStatus.COMPLETED) {
            log.info("!!! JOB FINISHED! Time to verify the results");

            jdbcTemplate.query("SELECT first_name, last_name FROM people",
                    (rs, row) -> new Person(
                            rs.getString(1),
                            rs.getString(2))
            ).forEach(person -> log.info("Found <" + person + "> in the database."));
        }
    }
}

公式サイトのサンプルコードでは、コンストラクタの引数が「JdbcTemplate」ですが、テストコードを作りやすくするために、引数を「DataSource」に変更しています。

最後に、mainクラスでSpringBatchを起動できるようになれば完成です。

package com.example.batchprocessing;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class BatchProcessingApplication {

	public static void main(String[] args) throws Exception {
		System.exit(SpringApplication.exit(SpringApplication.run(BatchProcessingApplication.class, args)));
	}
}

なお、Postgresの設定は、application.propertiesに記載し、sample-data.csvは、src/main/resources は以下に配置しています。

# PostgreSQLの接続先
spring.datasource.driverClassName = org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:5432/testdb
spring.datasource.username=postgres(任意のusername)
spring.datasource.password=xxxxx(任意のpassword)
Jill,Doe
Joe,Doe
Justin,Doe
Jane,Doe
John,Doe

Postgresの環境構築について知りたい方は以下をご覧ください。

【エンジニア向け】Macbook + docker-composeでpostgresを動かす

ここまで出来たら、ターミナルから、以下のコマンドで処理を実行しましょう。

cd [プロジェクト内のgradlewが存在するフォルダ]
./gradlew build

!!! JOB FINISHED! Time to verify the results”

という文字列がコンソールに出力され、DBに「sample-data.csv」で記載したユーザーが登録されていれば成功です。

4. Spring Batchのテスト自動化

ここまでくればあともう一歩です。

最後に、テストコードを書いてSpringBatchのテストを自動化させましょう。

プロジェクトコードと同じパッケージ構造をtestフォルダ内に作ります。

com.example.batchprocessing

テストコードを作る際にはspringbootTestは利用せずspringBatchTestのみでテストを実行させてみます。

そのためDBへのアクセスなどの設定は、application.propertiesではなく、javaのConfigurationファイルで定義します。

package com.example.batchprocessing;

import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

@Configuration
public class TestConfiguration {

    private final static String DRIVER_CLASS_NAME = "org.postgresql.Driver";
    private final static String URL = "jdbc:postgresql://localhost:5432/testdb";
    private final static String USER_NAME = "postgres";
    private final static String PASSWORD = "example";

    @Bean
    DataSource dataSource() {
        return DataSourceBuilder
                .create()
                .driverClassName(DRIVER_CLASS_NAME)
                .url(URL)
                .username(USER_NAME)
                .password(PASSWORD)
                .build();
    }
}

上記のクラスを用意することで、テスト中のデータアクセスを実現します。

次に、今回作成したSpringBatchに対するテストコードを追加します。

package com.example.batchprocessing;

import org.junit.Before;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.test.JobLauncherTestUtils;
import org.springframework.batch.test.context.SpringBatchTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;

@SpringBatchTest
@ContextConfiguration(classes = {BatchConfiguration.class, JobCompletionNotificationListener.class, TestConfiguration.class})
public class ImportUserJobTest {
    @Autowired
    private Job importUserJob;

    @Autowired
    private JobLauncherTestUtils jobLauncherTestUtils;

    @Before
    public void before() {
        jobLauncherTestUtils.setJob(importUserJob);
    }
    @Test
    public void testJob() throws Exception {

        JobExecution jobExecution = jobLauncherTestUtils.launchJob();


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

ここで重要なのは、@ContextConfigurationアノテーションです。

中身を見ると以下の3クラスが定義されています。

BatchConfiguration.class, JobCompletionNotificationListener.class, TestConfiguration.class

これにより、

今回テストしたいジョブの設定ファイル = BatchConfiguration

テスト中に呼び出されるListener = JobCompletionNotificationListener

テスト時のデータアクセスの設定 = TestConfiguration

の3つが選定されました。もし他のジョブをテストしたい場合などは上記で呼び出すクラスを変更する必要があります。

そして次に見るべき項目が、jobLauncherTestUtilsです。

jobLauncherTestUtils.setJob(importUserJob)

これは、指定したジョブをテスト環境で実行できるようにするUtilクラスです。

ここで指定されたジョブ = importUserJobは、まさにサンプルコードで用意したジョブになります。

最後はテストクラス内でのジョブの実行処理です。

jobLauncherTestUtils.launchJob();

上記メソッドを実行する際には、必ず事前に「jobLauncherTestUtils」に対してsetJobメソッドを実行している必要があります。

これで準備完了です。jUnitを実行してみください。

無事、テストが実行できれば成功です。

この情報が皆さんのお役に立てれば嬉しいです。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です