Mockito ArgumentCaptorの使い方:引数を検証するユニットテスト

ユニットテストでは、依存先のメソッドが呼ばれたことだけでなく、「どんな値で呼ばれたか」まで確認したい場面があります。たとえばサービスが入力を整形して User を作り、リポジトリへ保存を依頼する処理です。

Mockito の ArgumentCaptor を使うと、モックに渡された引数を取り出し、通常の JUnit アサーションで中身を検証できます。本記事では JUnit 5 と Mockito を使い、実行済みのサンプルをもとに基本形と使いどころを紹介します。

この記事で分かること

  • ArgumentCaptor でモックへ渡した引数を取得する方法
  • verify と組み合わせて、呼び出し回数と引数の内容を検証する方法
  • 複数回の呼び出しで getAllValues() を使う方法
  • equals()ArgumentCaptorargThat の使い分け

ArgumentCaptor が必要になる場面

Mockito は、引数を equals() で比較する通常の検証をまず推奨しています。引数オブジェクトに適切な equals() があり、オブジェクト全体を比較したいなら次のように書けます。

verify(userRepository).save(expectedUser);

一方で、テスト対象が内部で組み立てたオブジェクトの一部だけを確認したい場合があります。

  • 表示名は前後の空白を除去しているか
  • メールアドレスを小文字化しているか
  • 登録日時が指定した Clock から作られているか
  • 比較対象に含めたくない項目があるか

このようなときは、モックへ渡された実際の引数を捕捉してから、必要な項目だけをアサートすると読みやすくなります。

サンプルの構成

例として、入力値から User を作って UserRepository に保存する UserService をテストします。検証用コードは java-snippets の ArgumentCaptor サンプル にあります。

import java.time.Instant;

public record User(String name, String emailAddress, Instant createdAt) {
}
public interface UserRepository {
    void save(User user);
}
import java.time.Clock;
import java.time.Instant;
import java.util.Locale;

public class UserService {
    private final UserRepository userRepository;
    private final Clock clock;

    public UserService(UserRepository userRepository, Clock clock) {
        this.userRepository = userRepository;
        this.clock = clock;
    }

    public void register(String name, String emailAddress) {
        User user = new User(
                name.trim(),
                emailAddress.toLowerCase(Locale.ROOT),
                Instant.now(clock));
        userRepository.save(user);
    }
}

メールアドレスを小文字化するときは、実行環境のロケールに影響されないよう Locale.ROOT を指定しています。また Clock をコンストラクタから渡しているため、時刻をテストで固定できます。

基本形:引数を捕捉してから中身をアサートする

@Captor を付けたフィールドを用意し、verify の引数位置で capture() を呼びます。その後、getValue() で捕捉した値を取り出せます。MockitoExtension を使うと、@Mock@Captor を初期化できます。

次はテストクラスから、引数を捕捉して検証する部分だけを抜き出したものです。userRepositoryuserCaptoruserService の宣言と Clock.fixed による初期化、必要な import を含む完全なコードは 検証用サンプル を参照してください。

// @Captor ArgumentCaptor<User> userCaptor; をテストクラスのフィールドに宣言する

userService.register("  Alice  ", "ALICE@EXAMPLE.COM");

verify(userRepository).save(userCaptor.capture());

User savedUser = userCaptor.getValue();
assertEquals("Alice", savedUser.name());
assertEquals("alice@example.com", savedUser.emailAddress());
assertEquals(FIXED_INSTANT, savedUser.createdAt());

verify(userRepository).save(...)save の呼び出しを検証します。呼び出しがなければこの行で失敗します。呼び出しがあった場合は、そのときの User が Captor に保存され、以降のアサーションで値を確認できます。

verify の既定回数は 1 回です。回数も明示したい場合は verify(userRepository, times(1)) と書けますが、この例では省略しても意味は変わりません。

@Captor を使わない場合は、テストメソッド内で次のように作ることもできます。

ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
verify(userRepository).save(userCaptor.capture());

1 回しか使わない Captor ならこの書き方も分かりやすく、複雑なジェネリクス型をフィールドで宣言したい場合は @Captor が便利です。

複数回呼ばれる場合はすべての捕捉値を取得する

同じメソッドが複数回呼ばれる場合、getValue() は最後に捕捉した値を返します。すべての引数を確認したいときは、呼び出し回数を times で検証し、getAllValues() を使います。次はテストメソッド内の要点だけを示した抜粋です。

userService.register("  Alice  ", "ALICE@EXAMPLE.COM");
userService.register(" Bob ", "BOB@EXAMPLE.COM");

verify(userRepository, times(2)).save(userCaptor.capture());

List<User> savedUsers = userCaptor.getAllValues();
assertEquals(
        List.of(
                new User("Alice", "alice@example.com", FIXED_INSTANT),
                new User("Bob", "bob@example.com", FIXED_INSTANT)),
        savedUsers);

このテストでは、record が生成する equals() を利用して、捕捉順と各 User の内容をまとめて検証しています。順番が仕様として重要でないなら、順序を前提にしないアサーションへ変えるか、そもそも呼び出し順をテストする必要があるかを見直します。

equals、ArgumentCaptor、argThat の使い分け

どれか一つに統一する必要はありません。確認したいことに合わせて選びます。

ArgumentCaptor は、引数を捕捉してから詳しく確認するためのものです。スタブで「この条件の引数ならこの値を返す」としたい場合は、argThat などの引数 matcher のほうが意図を表しやすくなります。

注意点

Captor は検証の中で使う

capture()verify の引数として使います。Mockito 公式 Javadoc でも、ArgumentCaptor はスタブではなく検証で使うことが推奨されています。スタブで使うと、呼び出しがなかった場合に何も捕捉されず、失敗箇所も分かりにくくなります。

// 推奨しない例: スタブの条件に Captor を使う
when(client.send(captor.capture())).thenReturn(response);

// 条件付きのスタブなら matcher を使う
when(client.send(argThat(request -> request.isValid())).thenReturn(response);

検証しすぎない

Captor で取得できるからといって、すべてのフィールドを検証する必要はありません。テスト対象の責務に関係する値に絞ります。

この UserService の責務は、名前のトリミング、メールアドレスの小文字化、作成日時の設定です。そのためサンプルではこの 3 項目を検証しています。一方、User が単に受け取った値を代入するだけなら、その実装詳細まで繰り返し確認すると、変更に弱いテストになります。

matcher と生の値を混ぜない

capture() は Mockito の matcher として扱われます。同じメソッド呼び出しで matcher を使う場合、ほかの引数も matcher にします。

// 正しい: 両方とも matcher
verify(auditRepository).save(eq("CREATE"), captor.capture());

// 誤り: matcher と生の値を混在させている
verify(auditRepository).save("CREATE", captor.capture());

検証した環境と結果

記事のコード例は、java-snippets の検証用サンプル で実行しました。

  • JUnit: JUnit BOM 5.12.2
  • Mockito: mockito-junit-jupiter 5.23.0
  • Gradle wrapper: 9.5.1
  • 実行したコマンド: ./gradlew check -PjavaVersion=17./gradlew check -PjavaVersion=21./gradlew check -PjavaVersion=25
  • 結果: いずれも成功

まとめ

ArgumentCaptor は、モックに渡された引数を取り出し、通常の JUnit アサーションで確認するための仕組みです。

  • まずは equals() によるシンプルな検証で足りるか考える
  • 引数の一部や生成結果を詳しく確認したいときに ArgumentCaptor を使う
  • verify(mock).method(captor.capture()) の後に getValue() を使う
  • 複数回の呼び出しは timesgetAllValues() を組み合わせる
  • スタブの条件には Captor より argThat などの matcher を使う

この形を覚えておくと、サービス層が組み立てたコマンド、永続化するエンティティ、外部 API に送るリクエストなどを、実装の意図に沿って検証しやすくなります。

参考

コメントする

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)