【Spring Batch ハンズオン】単体テスト徹底解説!ItemProcessorの単体テストでビジネスロジックを徹底検証する

はじめに: Spring Batchテストの重要性とこの記事で学ぶこと

Spring Batchアプリケーションにおいて、ItemProcessorはバッチ処理の中核となるビジネスロジックを担う重要なコンポーネントです。

本記事では、ItemProcessorの単体テストに焦点を当て、ビジネスロジック、データ変換、フィルタリングが期待通りに機能するかを検証する方法をハンズオン形式で解説します。


目次

  1. Spring BatchにおけるItemProcessorの役割
    1. ItemProcessorの役割と重要性
    2. ItemProcessorのユースケース(補足)
  2. なぜItemProcessorの単体テストが必要なのか?
  3. テスト戦略の概要
  4. テスト対象のItemProcessorと関連エンティティ
    1. Productエンティティ
    2. MyItemProcessorクラス
  5. テスト環境の準備
    1. 依存関係の追加
    2. テストクラスの基本構造
  6. ItemProcessorの単体テスト実装
    1. テストケース1: サービス依存のテスト
    2. テストケース2: 正常系のテスト(PREMIUM判定)
    3. テストケース3: フィルタリングのテスト
    4. テストケース4: null入力のテスト
  7. テストの実行と結果確認
  8. 【発展】ItemProcessorの設計パターンと例外処理
    1. 複数のItemProcessorをチェーンする場合のテスト
    2. ItemProcessorにおける例外処理とテスト
  9. まとめ: ItemProcessorテストで堅牢なビジネスロジックを構築しよう

1. Spring BatchにおけるItemProcessorの役割

1-1. ItemProcessorの役割と重要性

ItemProcessorは、ItemReaderから読み込んだデータを加工・変換し、ItemWriterに渡す役割を担うコンポーネントです。

  • ItemProcessorの基本:
    • ItemReaderから受け取ったアイテムに対して、ビジネスロジックを適用し、必要に応じてデータを変換します。
  • 主な機能:
    • ビジネスロジックの適用:
      • 読み込んだデータに対して、計算、データ補完、バリデーションなどのビジネスルールを適用します。
    • データ形式の変換:
      • ある形式のデータを別の形式に変換します(例: DTOからエンティティへの変換)。
    • 特定の条件に基づくデータのフィルタリング:
      • 処理対象外のデータや不正なデータの場合にnullを返すことで、そのアイテムを後続のItemWriterに渡さずにスキップさせることができます。
  • なぜ重要か:
    • ItemProcessorはバッチ処理の「頭脳」とも言える部分であり、最も複雑なビジネスロジックが集中しがちです。
    • ここでの処理がバッチ処理の最終的な結果に直接影響するため、その正確性が非常に重要となります。

1-2. ItemProcessorのユースケース(補足)

ItemProcessorは、上記で述べた基本的な機能以外にも、以下のような典型的な処理で活用されます。

  • データのエンリッチメント(Data Enrichment):
    • ItemReaderから読み込んだアイテムに対し、外部システム(データベース、Webサービス、ファイルなど)から追加の関連データを取得して付与します。
    • 例えば、商品IDから商品詳細情報を取得して付加する、顧客IDから顧客属性情報を付加するなどのケースが考えられます。
  • データの正規化・クレンジング(Data Normalization/Cleansing):
    • 入力データ内の不整合を修正したり、書式を統一したりして、データの品質を向上させます。
    • 例えば、文字列フィールドのトリミング、日付フォーマットの変換、特定のコード値のマッピングなどがこれに該当します。
  • 複雑な条件に基づく処理(Conditional Processing based on Complex Business Rules):
    • 複数のフィールドの値や、外部から取得した情報に基づいて、アイテムの内容や状態を動的に変更します。
    • 例えば、特定の地域の顧客に対してのみ割引を適用する、複数の条件を組み合わせて商品のカテゴリを決定するといった、複雑なビジネスルールを適用する場合に利用されます。

2. なぜItemProcessorの単体テストが必要なのか?

ItemProcessorの単体テストは、バッチ処理の品質と信頼性を保証するために不可欠です。

  • ビジネスロジックの正確性検証:
    • ItemProcessorはバッチ処理の中核となるビジネスロジックを実装するため、その正確性を保証することが最も重要です。
    • 単体テストにより、あらゆる入力パターンに対して期待通りの出力が得られるかを確認できます。
  • データ変換の検証:
    • 入力データが期待通りに変換されるか、エッジケース(null、空文字列、不正な値など)が適切に処理されるかを検証します。
  • フィルタリングロジックの検証:
    • 特定の条件を満たすデータが正しくスキップされるか(nullを返すか)、または処理されるかを検証します。フィルタリング条件の網羅性を確認できます。
  • 高速なフィードバック:
    • ItemProcessorは通常、外部依存を持たない純粋なJavaコードとして実装されるため、単体テストは非常に高速に実行可能です。
    • これにより、開発者はコード変更後すぐに結果を確認し、開発サイクルを高速化できます。
  • 問題の早期発見:
    • 複雑なロジジックに潜むバグを開発の早い段階で発見し、修正コストを大幅に削減します。

3. テスト戦略の概要

本記事では、ItemProcessorの単体テストに特化し、以下の戦略で進めます。

  1. テスト対象の特定:
    • MyItemProcessorをテスト対象とし、そのビジネスロジック(データ変換、フィルタリング)を明確にします。
  2. テストデータの準備:
    • ItemProcessorprocess()メソッドに渡す入力データ(Productエンティティ)を準備します。様々なシナリオ(正常系、フィルタリング対象、null入力など)をシミュレートします。
  3. 振る舞いの検証:
    • ItemProcessorprocess()メソッドの戻り値が期待通りであるかを検証します。変換後のデータの内容や、フィルタリングされた場合にnullが返されることなどを確認します。
  4. 依存関係のモック化:
    • MyItemProcessorProductServiceに依存します。このProductServiceを、Mockitoを用いてその依存関係をモック化します。
    • これにより、ItemProcessorのビジネスロジックのみを、外部システム(データベースなど)の状態に影響されずに独立してテストできます。

この戦略により、ItemProcessorのビジネスロジックが、外部環境に左右されずに正しく機能することを保証します。


4. テスト対象のItemProcessorと関連エンティティ

本記事では、前回の記事で定義したProductエンティティを使用し、そのProductエンティティを加工するMyItemProcessorをテスト対象とします。


4-1. Productエンティティ

以下は、テストで使用するProductエンティティの簡略版です。

Product.java

package com.example.springbatchh2crud.model;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@Table(name = "products")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Product {

    @Id
    private Long id;

    private String name;
    private BigDecimal price;
    private String status;
    private boolean invalid;
}

Lombokを使用しない場合は、上記のように複数のコンストラクタを定義することで、様々な状況に対応できます。特にテストコードでは、必要なデータのみを持つインスタンスを生成できるため、テストデータの準備が容易になります。


テスト対象の仕様

今回テスト対象とするMyItemProcessorは、以下の仕様に基づいて動作します。

このItemProcessorは、入力データの妥当性チェック、外部サービスと連携した在庫確認、価格に応じたステータス設定など、実践的なビジネスロジックをカプセル化しています。

仕様説明
入力Productエンティティ、またはnull
null入力の扱い入力がnullの場合、後続の処理を行わずnullを返します。
例外処理Productの価格が10000を超える場合、IllegalArgumentExceptionをスローして処理を中断します。
フィルタリングProductの価格が100未満の場合、そのProductは処理対象外とみなし、nullを返して後続のItemWriterに渡しません(スキップします)。
ビジネスロジック(外部サービス依存)productService.checkStock()を呼び出し、戻り値がfalse(在庫なし)の場合、Productのステータスを"OUT_OF_STOCK"に設定します。
ビジネスロジック(内部処理)Productの価格が5000以上の場合、Productのステータスを"PREMIUM"に設定します。

ItemProcessorのBean定義 (MyProductJobConfigから抜粋)

MyItemProcessorは、ProductServiceに依存しており、Springの設定クラス内で以下のようにBeanとして定義されます。

// MyProductJobConfig.java (抜粋)
import com.example.springbatchh2crud.processor.MyItemProcessor;
import com.example.springbatchh2crud.service.ProductService;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyProductJobConfig {

    @Bean
    public ItemProcessor<Product, Product> myItemProcessor(
        ProductService productService
    ) {
        return new MyItemProcessor(productService);
    }
    // ... その他のBean定義
}

4-2. MyItemProcessorクラス

以下が、今回テスト対象となるMyItemProcessorの完全なコードです。
上記の仕様が、コード内でどのように実装されているかを確認しましょう。

MyItemProcessor.java

package com.example.springbatchh2crud.processor;

import com.example.springbatchh2crud.model.Product;
import com.example.springbatchh2crud.service.ProductService;
import java.math.BigDecimal;
import org.springframework.batch.item.ItemProcessor;

public class MyItemProcessor implements ItemProcessor<Product, Product> {

    private final ProductService productService;

    public MyItemProcessor(ProductService productService) {
        this.productService = productService;
    }

    @Override
    public Product process(Product product) throws Exception {
        // nullチェック
        if (product == null) {
            return null;
        }

        // 例外処理ロジック: 価格が10000を超える場合は例外をスロー
        if (product.getPrice().compareTo(BigDecimal.valueOf(10000)) > 0) {
            throw new IllegalArgumentException(
                "Price is too high for product: " + product.getId()
            );
        }

        // フィルタリングロジック: 価格が100未満の商品は処理しない
        if (product.getPrice().compareTo(BigDecimal.valueOf(100)) < 0) {
            return null; // nullを返すと、このアイテムはWriterに渡されない
        }

        // ビジネスロジック1: 在庫チェック
        boolean isInStock = productService.checkStock(product.getId());
        if (!isInStock) {
            product.setStatus("OUT_OF_STOCK");
        }

        // ビジネスロジック2: 価格に応じたステータス設定
        if (product.getPrice().compareTo(BigDecimal.valueOf(5000)) >= 0) {
            product.setStatus("PREMIUM");
        }

        return product;
    }
}

5. テスト環境の準備

5-1. 依存関係の追加

ItemProcessorの単体テストを行うために、以下の依存関係をpom.xml(Mavenの場合)またはbuild.gradle(Gradleの場合)に追加します。

spring-batch-testItemProcessorの単体テストでは必須ではありませんが、他のテストで必要になるため、通常は追加しておきます。

Mavenの場合 (pom.xml):

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <scope>test</scope>
</dependency>

Gradleの場合 (build.gradle):

testImplementation 'org.mockito:mockito-junit-jupiter'
testImplementation 'org.junit.jupiter:junit-jupiter-api'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
testImplementation 'org.assertj:assertj-core'

5-2. テストクラスの基本構造

ItemProcessorの単体テストを行うための基本的なテストクラスの構造

Mockitoを使ってItemProcessorの単体テストを行うための基本的なテストクラスの構造は以下のようになります。
@ExtendWith(MockitoExtension.class)でMockitoを有効にし、@Mockで依存オブジェクトをモック化します。

モック化とは、テスト対象のコンポーネントが依存する外部コンポーネント(データベース、外部サービスなど)を「模擬的なオブジェクト(モック)」に置き換えることです。
これにより、テスト対象のロジックのみを独立して、高速かつ安定して検証できます。

MyItemProcessorUnitTest.java

package com.example.springbatchh2crud.processor;

/**
 * MyItemProcessorの単体テストクラス。
 * Mockitoを利用して、依存する外部コンポーネントをモック化してテストを行います。
 */
@ExtendWith(MockitoExtension.class)
class MyItemProcessorUnitTest {

    // --- モックオブジェクト定義 ---
    @Mock
    private ProductService productService;

    @InjectMocks
    private MyItemProcessor myItemProcessor;

    // --- テストメソッド ---
    // (ここにテストケースを記述していく)
}

モック化前: 本番環境での依存関係

本番環境では、MyItemProcessorは実際のProductServiceに依存し、ProductServiceはさらにデータベースなどの外部システムに依存します。

ItemProcessorモック化前: 本番環境での依存関係

モック化後: テスト環境での依存関係

単体テストでは、MyItemProcessorが依存するProductServiceをモックオブジェクトに置き換えます。

  • @Mock private ProductService productService;
  • @InjectMocks private MyItemProcessor processor;

@InjectMocksMyItemProcessorのインスタンスを生成する際に、@Mockで作成されたproductServiceモックを自動的に注入します。

ItemProcessorモック化後: テスト環境での依存関係

これにより、MyItemProcessorのビジネスロジックが、外部環境に左右されずに正しく機能することを保証します。


@Mock@InjectMocksの役割

  • @Mock:
    • このアノテーションをフィールドに付与すると、Mockitoがその型のモックオブジェクトを自動的に生成し、テスト実行前に注入してくれます。
    • ItemProcessorが依存する外部サービス(例: リポジトリ、別のサービス)がある場合に利用します。
    • モックオブジェクトは、実際のオブジェクトの振る舞いをシミュレートするために使用され、特定のメソッド呼び出しに対して定義した値を返したり、例外をスローしたりすることができます。
  • @InjectMocks:
    • このアノテーションをテスト対象のクラスのフィールドに付与すると、Mockitoがそのインスタンスを自動的に生成し、@Mockで作成されたモックオブジェクトをそのインスタンスのコンストラクタやフィールドに自動的に注入してくれます。
    • これにより、テスト対象のクラスの初期化と依存関係の解決が容易になります。
    • @InjectMocksは、テスト対象のクラスが持つ依存関係を自動的に解決してくれるため、テストコードの記述量を減らし、可読性を向上させます。

ProductServiceの「アプリの仕様」とモック化の必要性

MyItemProcessorは、本記事の例では外部依存を持たない純粋なビジネスロジックとして実装されています。
しかし、実際のアプリケーションでは、ItemProcessorがデータベースへのアクセスや外部APIとの連携を行うサービスに依存することはよくあります。

例えば、ProductServiceというサービスが、Productエンティティの永続化や、他のシステムとのデータ同期などの役割を担っていると仮定します。
MyItemProcessorが処理したProductをこのProductServiceに渡して最終的な処理を行わせる場合、MyItemProcessorProductServiceに依存することになります。

このような場合、MyItemProcessorの単体テストを行う際に、実際のProductService(およびその先のデータベースや外部システム)に接続していては、テストが遅くなったり、外部環境に左右されたりしてしまいます。
そこで、ProductServiceをモック化することで、MyItemProcessorのロジックのみを独立して検証できるようになります。


6. ItemProcessorの単体テスト実装

それでは、MyItemProcessorUnitTestクラスに具体的なテストメソッドを実装していきましょう。


6-1. テストケース1: サービス依存のテスト

テストケースの仕様

  • シナリオ: 在庫がない商品(IDが奇数)が渡された場合。
  • 前提条件: 依存するProductServicecheckStock()メソッドがfalseを返すようにモックを設定します。
  • 検証ポイント: process()メソッドの戻り値であるProductstatusフィールドが"OUT_OF_STOCK"に設定されていることを検証します。

コード

MyItemProcessorUnitTest.java

@Test
@DisplayName("サービス依存テスト: 在庫なしの場合、ステータスがOUT_OF_STOCKに設定される")
void testProcess_withServiceDependency_OutOfStock() throws Exception {
    // Arrange
    Product product = Product.builder()
        .id(1L)
        .name("Test Product")
        .price(new BigDecimal("1500"))
        .build();
    when(productService.checkStock(anyLong())).thenReturn(false);

    // Act
    Product result = myItemProcessor.process(product);

    // Assert
    assertNotNull(result);
    assertEquals("OUT_OF_STOCK", result.getStatus());
}

解説

when(productService.checkStock(anyLong())).thenReturn(false); の部分で、ProductServiceモックの振る舞いを定義しています。
これにより、MyItemProcessor内のproductService.checkStock()が呼び出された際に必ずfalseが返され、在庫なしのロジックパスが実行されることを保証します。


6-2. テストケース2: 正常系のテスト(PREMIUM判定)

テストケースの仕様

  • シナリオ: 在庫があり、かつ価格が5,000円以上の商品が渡された場合。
  • 前提条件: productService.checkStock()trueを返すようにモックを設定します。
  • 検証ポイント: Productstatusフィールドが"PREMIUM"に設定されていることを検証します。

コード

MyItemProcessorUnitTest.java

@Test
@DisplayName("正常系テスト: 価格が5000以上の場合、ステータスがPREMIUMに設定される")
void testProcess_success_premium() throws Exception {
    // Arrange
    Product product = Product.builder()
        .id(2L)
        .name("Premium Product")
        .price(new BigDecimal("5500"))
        .build();
    when(productService.checkStock(anyLong())).thenReturn(true);

    // Act
    Product result = myItemProcessor.process(product);

    // Assert
    assertNotNull(result);
    assertEquals("PREMIUM", result.getStatus());
}

解説

このテストでは、在庫があり(checkStock()true)、かつ価格が5,000円以上という条件を満たした場合に、statusが正しく"PREMIUM"に更新されるかを確認します。


6-3. テストケース3: フィルタリングのテスト

テストケースの仕様

  • シナリオ: 処理対象外とすべき、価格が100円未満の商品が渡された場合。
  • 検証ポイント: process()メソッドがnullを返し、そのアイテムが後続の処理から除外(スキップ)されることを検証します。

コード

MyItemProcessorUnitTest.java

@Test
@DisplayName("フィルタリングテスト: 価格が100未満の場合、nullが返される")
void testProcess_filterItem() throws Exception {
    // Arrange
    Product product = Product.builder()
        .id(3L)
        .name("Filtered Product")
        .price(new BigDecimal("99"))
        .build();

    // Act
    Product result = myItemProcessor.process(product);

    // Assert
    assertNull(result);
}

解説

ItemProcessornullを返すと、そのアイテムはItemWriterに渡されずにスキップされます。このテストは、フィルタリングのロジックが正しく機能していることを保証します。


6-4. テストケース4: null入力のテスト

テストケースの仕様

  • シナリオ: ItemReaderが読み込みを終了した場合など、process()メソッドにnullが渡された場合。
  • 検証ポイント: NullPointerExceptionなどのエラーが発生せず、安全にnullが返されることを検証します。

コード

MyItemProcessorUnitTest.java

@Test
@DisplayName("null入力テスト: 入力がnullの場合、nullが返される")
void testProcess_nullInput() throws Exception {
    // Act
    Product result = myItemProcessor.process(null);

    // Assert
    assertNull(result);
}

解説

processメソッドの冒頭でnullチェックを行うことで、予期せぬNullPointerExceptionを防ぐことができます。このテストは、その防御的な実装が正しく機能していることを確認します。


7. テストの実行と結果確認

作成した単体テストは、お使いのIDE(IntelliJ IDEA, VS Codeなど)から直接実行できます。

  1. IDEでの実行: テストクラスまたはテストメソッドの横に表示される実行ボタン(通常は緑色の矢印)をクリックします。
  2. コマンドラインでの実行:
    • Maven: プロジェクトのルートディレクトリで./mvnw testを実行します。
    • Gradle: プロジェクトのルートディレクトリで./gradlew testを実行します。
  3. 結果の確認: テストが成功すると、IDEのテスト結果ウィンドウやコマンドラインの出力に成功を示す表示(緑色のバーなど)が表示されます。失敗した場合は、赤色の表示とともに、どのテストが失敗し、どのようなアサーションエラーが発生したかが詳細に表示されます。

これにより、ItemProcessorのビジネスロジック、データ変換、フィルタリングロジックが期待通りに機能しているかを確認できます。


8. 【発展】ItemProcessorの設計パターンと例外処理

ItemProcessorは、単一のビジネスロジックを適用するだけでなく、複数の処理を組み合わせたり、特定の条件下で例外を適切に処理したりする場面も多くあります。
ここでは、より複雑なItemProcessorの設計パターンと、それらのテスト方法について解説します。


8.1. 複数のItemProcessorをチェーンする場合のテスト

Spring Batchでは、CompositeItemProcessorを使用して複数のItemProcessorをチェーンし、順次処理を適用することができます。

これにより、各ItemProcessorは単一の責任を持つように設計でき、コードの再利用性と保守性が向上します。


CompositeItemProcessorの定義例

注記: 以下のサンプルコードは、CompositeItemProcessorの機能を示すための独立した例です。本記事でテストしてきたMyItemProcessorのロジックとは直接関係ありません。

// MyProductJobConfig.java (抜粋)
@Configuration
public class MyProductJobConfig {

    @Bean
    public ItemProcessor<Product, Product> compositeProductProcessor() {
        CompositeItemProcessor<Product, Product> compositeProcessor =
            new CompositeItemProcessor<>();
        compositeProcessor.setDelegates(
            Arrays.asList(
                new ProductUpperCaseProcessor(),
                new ProductPriceIncrementProcessor()
            )
        );
        return compositeProcessor;
    }

    // Composite Processor用の内部クラス
    public static class ProductUpperCaseProcessor
        implements ItemProcessor<Product, Product> {

        @Override
        public Product process(Product item) {
            if (item == null) return null;
            item.setName(item.getName().toUpperCase());
            return item;
        }
    }

    public static class ProductPriceIncrementProcessor
        implements ItemProcessor<Product, Product> {

        @Override
        public Product process(Product item) {
            if (item == null) return null;
            item.setPrice(item.getPrice().add(BigDecimal.valueOf(100)));
            return item;
        }
    }
    // ...
}

CompositeItemProcessorのテスト戦略

CompositeItemProcessorをテストする場合、個々のデリゲート(ProductUpperCaseProcessorなど)がそれぞれ単体テストされていることを前提に、CompositeItemProcessorがそれらを正しく連結し、最終的な結果が期待通りになるかを検証します。

テスト例

以下のテストでは、CompositeItemProcessorに実際のデリゲートクラスのインスタンスを設定し、一連の処理がすべて適用された最終結果を検証しています。

package com.example.springbatchh2crud.processor;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import com.example.springbatchh2crud.model.Product;
import java.math.BigDecimal;
import java.util.Arrays;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.batch.item.support.CompositeItemProcessor;

class CompositeProductProcessorUnitTest {

    private CompositeItemProcessor<Product, Product> compositeProcessor;

    // テスト対象のプロセッサ1: 商品名を大文字にする
    static class ProductUpperCaseProcessor
        implements ItemProcessor<Product, Product> {

        @Override
        public Product process(Product item) throws Exception {
            if (item == null) return null;
            item.setName(item.getName().toUpperCase());
            return item;
        }
    }

    // テスト対象のプロセッサ2: 価格を100増やす
    static class ProductPriceIncrementProcessor
        implements ItemProcessor<Product, Product> {

        @Override
        public Product process(Product item) throws Exception {
            if (item == null) return null;
            item.setPrice(item.getPrice().add(BigDecimal.valueOf(100)));
            return item;
        }
    }

    @BeforeEach
    void setUp() {
        // CompositeItemProcessorをセットアップ
        compositeProcessor = new CompositeItemProcessor<>();
        compositeProcessor.setDelegates(
            Arrays.asList(
                new ProductUpperCaseProcessor(),
                new ProductPriceIncrementProcessor()
            )
        );
    }

    @Test
    @DisplayName("CompositeProcessorテスト: 複数のプロセッサが順に適用される")
    void testCompositeProcessor_success() throws Exception {
        // Arrange
        Product product = Product.builder()
            .name("Sample Product")
            .price(new BigDecimal("1000"))
            .build();

        // Act
        Product result = compositeProcessor.process(product);

        // Assert
        assertNotNull(result);
        assertEquals(
            "SAMPLE PRODUCT",
            result.getName(),
            "商品名が大文字に変換されていること"
        );
        assertEquals(
            0,
            new BigDecimal("1100").compareTo(result.getPrice()),
            "価格が100増加していること"
        );
    }
}

解説:
このテストでは、CompositeItemProcessorに実際のデリゲートのインスタンスをセットしています。
そして、一連の処理(大文字化と価格加算)がすべて適用された最終的な結果をアサーションで検証します。
個々のデリゲートプロセッサ(ProductUpperCaseProcessorなど)は、それぞれ別途単体テストされていることが前提です。CompositeItemProcessorのテストでは、それらが正しく連結されて動作するかを検証することに焦点を当てます。


8.2. ItemProcessorにおける例外処理とテスト

ItemProcessor内でビジネスロジックの実行中に例外が発生する可能性は常にあります。
Spring Batchは、このような例外を適切に処理するためのメカニズムを提供しており、テストではその挙動を検証することが重要です。


例外発生時の挙動

ItemProcessor内で例外が発生した場合、デフォルトではそのアイテムはスキップされ、処理は次のアイテムに進みます。
ただし、Stepの設定によっては、特定の例外を再試行したり、処理を中断したりすることも可能です。


例外処理のテスト戦略

ItemProcessorの例外処理をテストする場合、以下の点を考慮します。

  1. 特定の入力で例外が発生することの検証:
    • ItemProcessorが特定の不正な入力に対して例外をスローすることを検証します。
  2. 例外が適切にラップされるか、またはそのままスローされるかの検証:
    • カスタム例外をスローする場合や、特定の例外を別の例外に変換する場合、その挙動を検証します。
  3. Spring Batchの例外処理メカニズムとの連携:
    • ItemProcessor単体テストの範囲外ですが、Stepレベルでの例外処理(スキップ、再試行など)が期待通りに機能するかは、統合テストで検証する必要があります。

テスト例 (例外発生の検証)

MyItemProcessorは、価格が10,000円を超える場合にIllegalArgumentExceptionをスローします。この挙動をテストで検証します。


テストケースの仕様

このテストケースでは、MyItemProcessorが仕様通りに例外をスローすることを検証します。

  • シナリオ:
    1. 価格が10,000円を超えるProductエンティティを入力としてprocess()メソッドに渡します。
    2. MyItemProcessorIllegalArgumentExceptionをスローすることを確認します。
  • 検証ポイント:
    • process()メソッドがIllegalArgumentExceptionをスローすること。
    • スローされた例外のメッセージが期待される内容を含んでいること。

MyItemProcessorExceptionUnitTest.java

package com.example.springbatchh2crud.processor;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import com.example.springbatchh2crud.model.Product;
import com.example.springbatchh2crud.service.ProductService;
import java.math.BigDecimal;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class MyItemProcessorExceptionUnitTest {

    @Mock
    private ProductService productService; // MyItemProcessorが依存しているためMockが必要

    @InjectMocks
    private MyItemProcessor myItemProcessor;

    @Test
    @DisplayName(
        "例外処理テスト: 価格が10000を超える場合、IllegalArgumentExceptionがスローされる"
    )
    void testProcess_throwsExceptionForHighPrice() {
        // Arrange
        Product product = Product.builder()
            .id(104L)
            .name("Exception Product")
            .price(new BigDecimal("12000"))
            .build();

        // Act & Assert
        IllegalArgumentException exception = assertThrows(
            IllegalArgumentException.class,
            () -> {
                myItemProcessor.process(product);
            }
        );

        assertEquals(
            "Price is too high for product: 104",
            exception.getMessage()
        );
    }
}

解説:

  • JUnit 5のassertThrows()メソッドを使用して、指定されたコードブロック(ラムダ式)が特定の例外(この場合はIllegalArgumentException)をスローすることを検証します。
  • assertThrows()はスローされた例外オブジェクトを返すため、その後のassertEquals()で例外メッセージが期待通りであることも併せて確認できます。これにより、期待通りの例外が、期待通りの理由で発生したことを保証します。

これらの発展的なテスト手法を理解することで、より堅牢で信頼性の高いItemProcessorを開発し、Spring Batchアプリケーション全体の品質を向上させることができます。


9. まとめ: ItemProcessorテストで堅牢なビジネスロジックを構築しよう

本記事では、Spring BatchのItemProcessorの単体テストに焦点を当て、その重要性、Mockitoを用いたテスト手法、そして具体的なテストコードの実装方法を解説しました。

ItemProcessorはバッチ処理の「頭脳」とも言える部分であり、そのビジネスロジックの正確性がバッチ処理全体の成否を左右します。

  • ItemProcessorの単体テストは、バッチ処理の中核となるビジネスロジック、データ変換、フィルタリングロジックの正確性を保証するために不可欠です。 複雑なビジネス要件を確実に満たし、予期せぬ動作を防ぐためには、徹底した単体テストが最も効果的な手段となります。
  • 外部依存をモック化することで、純粋なビジネスロジックのみを高速にテストでき、開発の早い段階でバグを発見・修正できます。これにより、開発コストを大幅に削減し、品質の高いバッチアプリケーションを効率的に開発することが可能になります。
  • 本記事で紹介したテスト戦略と具体的な実装例は、あなたのItemProcessorが期待通りに機能することを保証し、堅牢で信頼性の高いバッチ処理を構築するための強力な基盤となるでしょう。

このアプローチを実践することで、自信を持ってSpring Batchアプリケーションを開発し、運用することができます。


免責事項

  • 本記事の内容は、公開時点での情報に基づいています。技術の進歩や仕様変更により、将来的に内容が変更される可能性があります。
  • 本記事に記載されている情報、コード例、およびそれらを利用した結果生じるいかなる損害についても、著者は一切の責任を負いません。
  • 読者の皆様ご自身の責任において、内容の正確性を確認し、ご利用くださいますようお願いいたします。
  • 特に、本番環境への適用や重要なシステムへの組み込みを行う際は、十分なテストと検証を行ってください。

SNSでもご購読できます。

コメントを残す

*