【Java】Mockitoでモックを使う方法を中級者向けに解説

Java

こんにちは、かつコーチです。
外部APIやデータベースに依存するクラスをテストしようとして、テスト環境の準備だけで疲れてしまった経験はありませんか。
今回はJavaのテストで広く使われるMockito(依存クラスの動きを偽物(モック)に差し替えられるライブラリ)の基本的な使い方を解説します。

Mockitoとは?

なぜモックが必要なのか

例えば「ユーザー情報をDBから取得して、条件に合えば通知を送る」というクラスをテストする場合、本物のDB接続やメール送信サーバーが必要になってしまいます。
テストのたびに外部システムを用意するのは非現実的ですし、外部の状態次第でテスト結果が変わってしまうと再現性のあるテストになりません。

モック(本物の代わりに動作する偽物のオブジェクト)を使うと、「このメソッドが呼ばれたらこの値を返す」という振る舞いだけを定義でき、外部システムなしでロジックだけを検証できます。
JUnit5と組み合わせて使うのが定番の構成です。

依存関係の追加

Mavenの場合、pom.xmlに次を追加します。

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <version>5.11.0</version>
    <scope>test</scope>
</dependency>

Gradleの場合は次を追加します。

testImplementation 'org.mockito:mockito-junit-jupiter:5.11.0'

mockito-junit-jupiterにはJUnit5との連携用アノテーションが含まれているため、単体のmockito-coreではなくこちらを選ぶのがおすすめです。

基本の書き方・実装手順

手順1:テスト対象と依存クラスを準備する

public interface UserRepository {
    User findById(long id);
}

public class NotificationService {

    private final UserRepository userRepository;

    public NotificationService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public String buildWelcomeMessage(long userId) {
        User user = userRepository.findById(userId);
        if (user == null) {
            throw new IllegalArgumentException("ユーザーが見つかりません");
        }
        return user.getName() + "さん、ようこそ!";
    }
}

NotificationServiceUserRepositoryに依存していますが、テストではDBに接続したくないので、このUserRepositoryをモックに差し替えます。

手順2:@Mockと@InjectMocksでモックを組み立てる

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;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class NotificationServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private NotificationService notificationService;

    @Test
    void buildWelcomeMessage_ユーザーが存在する場合はメッセージを返す() {
        User mockUser = new User(1L, "かつコーチ");
        when(userRepository.findById(1L)).thenReturn(mockUser);

        String message = notificationService.buildWelcomeMessage(1L);

        assertEquals("かつコーチさん、ようこそ!", message);
    }
}

@ExtendWith(MockitoExtension.class)を付けることでJUnit5とMockitoが連携し、@Mockが付いたフィールドに自動でモックが生成されます。
@InjectMocksは、テスト対象のクラスにモックをコンストラクタ経由で自動的に注入してくれるアノテーションです。
when(...).thenReturn(...)で「このメソッドがこの引数で呼ばれたらこの値を返す」という振る舞いを定義します。

手順3:verifyでメソッドの呼び出しを検証する

戻り値の検証だけでなく、「特定のメソッドが呼ばれたかどうか」を確認したい場合はverifyを使います。

import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.times;

@Test
void buildWelcomeMessage_findByIdが1回だけ呼ばれる() {
    User mockUser = new User(1L, "かつコーチ");
    when(userRepository.findById(1L)).thenReturn(mockUser);

    notificationService.buildWelcomeMessage(1L);

    verify(userRepository, times(1)).findById(1L);
}

verify(mock, times(1)).メソッド名(引数)という形で、指定したメソッドが指定回数呼ばれたかを検証できます。
不要なメソッド呼び出しがないかを確認したいときにも役立つ書き方です。

つまずきやすい設定・注意点

@Mockを付けたフィールドをコンストラクタに渡す順序と、テスト対象クラスのコンストラクタの引数順が一致していないと@InjectMocksが正しく注入できません。
フィールドが複数ある場合は、コンストラクタインジェクションの引数順とフィールドの型を揃えておくと安全です。

よくあるつまずきポイント・エラー対処

UnnecessaryStubbingExceptionでテストが失敗した

実際に私が案件でハマったのが、複数のテストケースで共通のwhenをまとめて書いていたときに発生したエラーです。

// ❌Before:使わないスタブが残っている
@BeforeEach
void setUp() {
    when(userRepository.findById(1L)).thenReturn(new User(1L, "太郎"));
    when(userRepository.findById(2L)).thenReturn(new User(2L, "花子"));
}

@Test
void test1() {
    // このテストではfindById(1L)しか使わない
    notificationService.buildWelcomeMessage(1L);
}

このコードを実行するとUnnecessaryStubbingException: Unnecessary stubbings detectedというエラーになりました。
Mockitoは「定義したのに一度も使われなかったスタブ」を厳格モードで検出し、テストを失敗させる仕様になっているためです。

// ✅After:各テストで必要なスタブだけをテストメソッド内に書く
@Test
void test1_findById1のユーザーでメッセージを作成する() {
    when(userRepository.findById(1L)).thenReturn(new User(1L, "太郎"));

    String message = notificationService.buildWelcomeMessage(1L);

    assertEquals("太郎さん、ようこそ!", message);
}

@BeforeEachに共通スタブをまとめたくなる気持ちは分かりますが、各テストで実際に使うスタブだけをテストメソッド内に書く方が、未使用スタブのエラーを避けられるうえにテストの意図も読みやすくなります。

応用・一歩先の使い方

例外を投げるモックを作る

import static org.mockito.Mockito.when;
import static org.junit.jupiter.api.Assertions.assertThrows;

@Test
void findById_存在しないIDで例外がスローされる() {
    when(userRepository.findById(999L))
        .thenThrow(new IllegalStateException("DB接続エラー"));

    assertThrows(IllegalStateException.class,
        () -> notificationService.buildWelcomeMessage(999L));
}

thenThrowを使うと、DB接続エラーのような異常系もモックで簡単に再現できます。
本物のDBを落とさなくても異常系のテストが書けるのは、モックを使う大きなメリットです。

引数の内容にこだわらずマッチさせるArgumentMatchers

import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.when;

when(userRepository.findById(anyLong())).thenReturn(new User(1L, "かつコーチ"));

引数の具体的な値にこだわらず「どんなlongが渡されてもこの値を返す」と定義したい場合はanyLong()などのArgumentMatchersを使います。
ただし、1つの引数にany()系を使う場合は他の引数もすべてany()系かMatcherで統一する必要がある点に注意してください。

まとめ

この記事のポイント

  • モックを使うと外部システムに依存せずロジックだけを検証できる
  • @Mockでモックを作り、@InjectMocksでテスト対象に自動注入する
  • when(...).thenReturn(...)で戻り値、verify(...)で呼び出し回数を検証する
  • 未使用のスタブはUnnecessaryStubbingExceptionの原因になるため、テストごとに必要な分だけ定義する

次に読むべき記事

  • アサーションの書き方
  • パラメータ化テスト

タグ: Java, 中級者向け, テスト

タイトルとURLをコピーしました