Mockitoを使いこなす!Java単体テストを効率化する完全ガイド

この記事から得られる知識

  • Mockitoの基本的な概念と、単体テストでモックがなぜ重要なのかを理解できる。
  • mock(), when(), verify() を使った基本的なモックの作成、振る舞いの設定、検証方法を習得できる。
  • 引数マッチャー(Argument Matchers)を使いこなし、より柔軟なテストケースを記述できるようになる。
  • @Mock@InjectMocksなどのアノテーションを活用し、テストコードをより簡潔かつ効率的に記述する方法がわかる。
  • 実オブジェクトの挙動を利用しつつ一部のメソッドだけを置き換える「スパイ(Spy)」の使い方を学べる。
  • メソッドに渡された引数をキャプチャして詳細なアサーションを行うArgumentCaptorの利用方法を理解できる。
  • 振る舞い駆動開発(BDD)スタイルに沿った、より可読性の高いテストコードの書き方を習得できる。
  • staticメソッドやfinalクラスといった、従来モック化が難しかった要素への対処法を知ることができる。

第1章: Mockitoとは? なぜ必要なのか?

Mockitoは、Javaアプリケーションの単体テスト(ユニットテスト)を作成するための非常に人気のあるオープンソースのモッキングフレームワークです。単体テストの目的は、テスト対象のクラスやメソッド(「System Under Test」またはSUT)を、それが依存する他のコンポーネントから隔離して検証することです。

しかし、多くのクラスは他のクラスと連携して動作します。例えば、データベースにアクセスするクラス、外部のWeb APIを呼び出すクラス、ファイルシステムを操作するクラスなどが考えられます。これらの依存オブジェクトをそのままテストで使うと、いくつかの問題が発生します。

依存オブジェクトをそのまま使う場合の問題点

  • 外部環境への依存: データベースやネットワークが利用できないとテストが失敗してしまう。
  • テストの低速化: データベースアクセスやAPI通信は時間がかかり、テスト全体の実行速度を低下させる。
  • 再現性の欠如: 外部APIのレスポンスが毎回変わる場合や、データベースの状態によって結果が変わり、テストが不安定になる。
  • 特定の状態の再現困難: データベースエラーやネットワーク切断といった、異常系の状況を意図的に作り出すのが難しい。

ここで登場するのがモックオブジェクトです。モックオブジェクトは、本物のオブジェクトのように振る舞う「偽物」のオブジェクトです。Mockitoを使うことで、このモックオブジェクトを驚くほど簡単に作成し、その振る舞いを自由に定義できます。

例えば、「データベースからユーザー情報を取得する」という依存オブジェクトのメソッドを、「’Taro Yamada’という名前のユーザー情報を返す」ように設定できます。これにより、実際にデータベースに接続することなく、テスト対象クラスがユーザー情報を正しく扱えるかどうかを検証できるのです。

Mockitoを活用することで、テストは高速安定的になり、外部環境に一切依存しなくなります。これにより、開発者は自信を持ってリファクタリングや機能追加を進めることができるようになります。

第2章: 環境構築

Mockitoを使い始めるには、まずプロジェクトにライブラリを追加する必要があります。ここでは、一般的なビルドツールであるMavenとGradleを使った設定方法を紹介します。JUnit 5 (Jupiter) と一緒に使う構成が現在の主流です。

Mavenでの設定

pom.xmlファイルの<dependencies>セクションに以下の依存関係を追加します。mockito-coreがMockito本体、mockito-junit-jupiterがJUnit 5と連携するためのライブラリです。

<dependencies>
  <!-- JUnit 5 -->
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.10.2</version>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.10.2</version>
    <scope>test</scope>
  </dependency>

  <!-- Mockito Core -->
  <dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>5.12.0</version>
    <scope>test</scope>
  </dependency>

  <!-- Mockito JUnit 5 Integration -->
  <dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <version>5.12.0</version>
    <scope>test</scope>
  </dependency>
</dependencies>

Gradleでの設定

build.gradleファイルのdependenciesブロックに以下を追加します。

dependencies {
    // JUnit 5
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.2'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.2'

    // Mockito Core
    testImplementation 'org.mockito:mockito-core:5.12.0'

    // Mockito JUnit 5 Integration
    testImplementation 'org.mockito:mockito-junit-jupiter:5.12.0'
}

test {
    useJUnitPlatform()
}

バージョンの確認: 上記のバージョンは執筆時点のものです。常に最新の安定版を使用することをお勧めします。Maven Centralなどのリポジトリで最新バージョンを確認してください。

第3章: モックの基本 3ステップ

Mockitoを使ったテストの基本的な流れは、以下の3つのステップで構成されます。非常にシンプルで直感的に記述できます。

  1. モックの作成: 依存するクラスのモックオブジェクトを生成する。
  2. 振る舞いの設定 (スタブ化): モックオブジェクトのメソッドが呼び出されたときに、どう振る舞うかを定義する。
  3. メソッド呼び出しの検証: テスト対象のメソッドを実行した後、モックオブジェクトのメソッドが期待通りに呼び出されたかを確認する。

ここでは、簡単なサンプルコードを通して具体的に見ていきましょう。ユーザー名を取得して挨拶を返すGreeterクラスと、その依存先であるUserServiceがあるとします。

// 依存されるクラス (例: データベースと通信する)
public interface UserService {
    String getUserName(int id);
}

// テスト対象クラス (SUT)
public class Greeter {
    private final UserService userService;

    public Greeter(UserService userService) {
        this.userService = userService;
    }

    public String greet(int id) {
        String name = userService.getUserName(id);
        if (name == null) {
            return "Hello, Anonymous!";
        }
        return "Hello, " + name + "!";
    }
}

このGreeterクラスを、UserServiceから隔離してテストします。

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

class GreeterTest {

    @Test
    void testGreetWithValidUser() {
        // 1. モックの作成
        // UserServiceインターフェースのモックオブジェクトを作成
        UserService mockUserService = mock(UserService.class);

        // 2. 振る舞いの設定 (スタブ化)
        // mockUserService.getUserName(1)が呼び出されたら、"Taro"を返すように設定
        when(mockUserService.getUserName(1)).thenReturn("Taro");

        // テスト対象クラスのインスタンス化 (モックを注入)
        Greeter greeter = new Greeter(mockUserService);

        // テスト実行
        String result = greeter.greet(1);

        // 結果のアサーション
        assertEquals("Hello, Taro!", result);

        // 3. メソッド呼び出しの検証 (任意)
        // mockUserService.getUserName(1)が1回呼び出されたことを確認
        verify(mockUserService, times(1)).getUserName(1);
    }

    @Test
    void testGreetWithNullUser() {
        // 1. モックの作成
        UserService mockUserService = mock(UserService.class);

        // 2. 振る舞いの設定
        // 存在しないID(99)で呼ばれた場合はnullを返すように設定
        when(mockUserService.getUserName(99)).thenReturn(null);

        Greeter greeter = new Greeter(mockUserService);

        // テスト実行
        String result = greeter.greet(99);

        // 結果のアサーション
        assertEquals("Hello, Anonymous!", result);

        // 3. 検証
        verify(mockUserService).getUserName(99); // times(1)はデフォルトなので省略可能
    }
}

3ステップのまとめ

  • mock(Class.class): 引数で指定されたクラスやインターフェースのモックオブジェクトを生成します。
  • when(mock.method()).thenReturn(value): when()にメソッド呼び出しを指定し、thenReturn()でその戻り値を定義します。これがスタブ化の基本形です。
  • verify(mock).method(): テストの最後に、対象のメソッドが期待通りに呼び出されたかを検証します。

第4章: スタブ化の詳細

when().thenReturn()はスタブ化の基本ですが、Mockitoはより複雑なシナリオに対応するための様々な機能を提供します。

戻り値のバリエーション

メソッド 説明
thenReturn(value) 指定された値を返します。最も一般的に使用されます。
thenReturn(value1, value2, ...) 呼び出されるたびに、指定された値を順番に返します。最後の値はそれ以降ずっと返されます。
thenThrow(new Exception()) 指定された例外をスローします。異常系のテストで必須です。
thenAnswer(Answer<T> answer) より複雑なロジックで戻り値を生成したい場合に使用します。引数に基づいて動的にレスポンスを変えることなどが可能です。
thenCallRealMethod() モックオブジェクトではなく、実際(本物)のメソッドを呼び出します。主にSpyオブジェクトと組み合わせて使用されます。

Voidメソッドのスタブ化

戻り値がない (void) メソッドで例外をスローさせたい場合は、when()の構文が使えません。代わりにdoThrow()を使います。

// 例: voidメソッドを持つリストのモック
List<String> mockList = mock(List.class);

// add(0, "item") を呼び出すと例外が発生するように設定
doThrow(new IllegalArgumentException("Invalid index!"))
    .when(mockList).add(0, "item");

// テストコード
assertThrows(IllegalArgumentException.class, () -> {
    mockList.add(0, "item");
});

引数マッチャー (Argument Matchers)

特定の引数値に固定せず、「任意の文字列」「任意の数値」といった、より柔軟な条件でスタブ化や検証を行いたい場合があります。そのために使われるのが引数マッチャーです。

重要なルール: 1つのメソッド呼び出しで引数マッチャーを使う場合、すべての引数をマッチャーで指定する必要があります。実値とマッチャーを混在させることはできません。

例: when(mock.someMethod(anyInt(), "specific string")) はエラーになります。正しくは when(mock.someMethod(anyInt(), eq("specific string"))) とします。

よく使われる引数マッチャー

マッチャー 説明 使用例
any() 任意のオブジェクト(nullも含む)にマッチします。 when(mock.method(any())).thenReturn(...)
any(Class.class) 指定されたクラスの任意のインスタンスにマッチします。 when(mock.method(any(String.class))).thenReturn(...)
anyInt(), anyString(), anyList(), etc. 各型に対応した任意のインスタンスにマッチします。any()よりタイプセーフです。 verify(mock).process(anyInt());
eq(value) マッチャーと実値を混ぜて使えないルールを回避するために、実値をマッチャーに変換します。 when(mock.method(anyInt(), eq("fixed"))).thenReturn(...)
notNull() nullでない任意のオブジェクトにマッチします。 verify(mock).save(notNull());
argThat(argument -> condition) カスタムの条件をラムダ式で記述できます。非常に強力です。 verify(mock).save(argThat(user -> user.getName().startsWith("T")));

第5章: 検証の詳細

verify()は、単にメソッドが呼ばれたかどうかを確認するだけでなく、呼び出された回数や順序など、より詳細なインタラクションを検証する機能も備えています。

呼び出し回数の検証

verify()の第二引数にVerificationModeを指定することで、呼び出し回数を検証できます。

List<String> mockList = mock(List.class);

mockList.add("one");
mockList.add("two");
mockList.add("two");

// "one"が1回だけ呼ばれたことを検証
verify(mockList).add("one"); // times(1)はデフォルト
verify(mockList, times(1)).add("one");

// "two"が2回呼ばれたことを検証
verify(mockList, times(2)).add("two");

// "three"が一度も呼ばれなかったことを検証
verify(mockList, never()).add("three");

// 最低1回呼ばれたことを検証
verify(mockList, atLeastOnce()).add("one");

// 最低2回呼ばれたことを検証
verify(mockList, atLeast(2)).add("two");

// 最大2回呼ばれたことを検証
verify(mockList, atMost(2)).add("two");

呼び出し順序の検証

複数のモックオブジェクトにまたがって、メソッドが正しい順序で呼び出されたかを検証したい場合はInOrderを使います。

// 2つのモックを作成
List<String> firstMock = mock(List.class);
List<String> secondMock = mock(List.class);

// モックを使用
firstMock.add("A");
secondMock.add("B");

// InOrderオブジェクトを作成
InOrder inOrder = inOrder(firstMock, secondMock);

// この順序で呼び出されたことを検証
inOrder.verify(firstMock).add("A");
inOrder.verify(secondMock).add("B");

タイムアウト付きの検証

非同期処理のテストなどで、指定した時間内にメソッドが呼び出されることを検証したい場合に使います。

// 100ミリ秒以内に someMethod() が呼ばれることを検証
verify(mock, timeout(100)).someMethod();

// 100ミリ秒以内に最低2回呼ばれることを検証
verify(mock, timeout(100).atLeast(2)).someMethod();

第6章: アノテーションでもっと便利に

これまで見てきたmock()メソッドは直感的ですが、テストクラス内に多くのモックが必要になると、記述が冗長になりがちです。Mockitoはアノテーションを使って、この初期化処理を劇的に簡潔にする方法を提供します。

アノテーションを有効にするには、テストクラスに@ExtendWith(MockitoExtension.class)を付与する必要があります。

主要なアノテーション

  • @Mock: mock(Class.class)の代わり。フィールドに付与するだけでモックオブジェクトが自動的に生成されます。
  • @InjectMocks: テスト対象のクラス(SUT)のインスタンスを生成し、@Mock@Spyで作成されたモックを自動的に注入(インジェクション)します。コンストラクタインジェクション、セッターインジェクション、フィールドインジェクションを試みます。
  • @Spy: 後述するSpyオブジェクトを生成します。
  • @Captor: 後述するArgumentCaptorを生成します。

アノテーションを使ったリファクタリング例

第3章のGreeterTestをアノテーションを使って書き換えてみましょう。

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.*;

// JUnit 5にMockito拡張を適用
@ExtendWith(MockitoExtension.class)
class GreeterAnnotationTest {

    // UserServiceのモックを自動生成
    @Mock
    private UserService mockUserService;

    // Greeterのインスタンスを生成し、@Mock化したmockUserServiceを自動で注入
    @InjectMocks
    private Greeter greeter;

    @Test
    void testGreetWithValidUser() {
        // モック作成とインスタンス化が不要になった!

        // 振る舞いの設定
        when(mockUserService.getUserName(1)).thenReturn("Hanako");

        // テスト実行
        String result = greeter.greet(1);

        // 結果のアサーション
        assertEquals("Hello, Hanako!", result);

        // 検証
        verify(mockUserService).getUserName(1);
    }
}

どうでしょうか。mock()の呼び出しやnew Greeter(...)がなくなり、テストメソッドが本質的なロジック(振る舞いの設定と検証)に集中できていることがわかります。テストクラスが大規模になるほど、この恩恵は大きくなります。

第7章: 高度な機能

基本的な使い方をマスターしたら、さらに強力な機能を使ってみましょう。これらを使いこなすことで、テスト可能な範囲が広がり、より複雑なシナリオにも対応できるようになります。

ArgumentCaptor: 引数を捕まえる

メソッドが呼び出された際の引数そのものを後から検証したいケースがあります。例えば、サービスがリポジトリのsaveメソッドを呼び出す際に、正しい内容を持つUserオブジェクトを渡しているか確認したい、などです。このような場合にArgumentCaptorが絶大な威力を発揮します。

// Userクラス
class User {
    private String name;
    private String email;
    // getters and setters
}

// UserRepositoryインターフェース
interface UserRepository {
    void save(User user);
}

// テスト対象のサービス
class UserRegistrationService {
    private final UserRepository userRepository;
    public UserRegistrationService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    public void register(String name, String email) {
        User newUser = new User();
        newUser.setName(name);
        newUser.setEmail(email);
        userRepository.save(newUser);
    }
}

// テストコード
@ExtendWith(MockitoExtension.class)
class UserRegistrationServiceTest {
    @Mock
    private UserRepository mockUserRepository;

    @InjectMocks
    private UserRegistrationService userRegistrationService;

    // ArgumentCaptorをフィールドで宣言
    @Captor
    private ArgumentCaptor<User> userCaptor;

    @Test
    void testRegister() {
        // サービスメソッドを実行
        userRegistrationService.register("Jiro", "jiro@example.com");

        // voidメソッドの検証時にキャプチャを指定
        verify(mockUserRepository).save(userCaptor.capture());

        // キャプチャした引数を取得
        User savedUser = userCaptor.getValue();

        // キャプチャしたオブジェクトの内容を詳細にアサーション
        assertEquals("Jiro", savedUser.getName());
        assertEquals("jiro@example.com", savedUser.getEmail());
    }
}

verify()の中でcaptor.capture()を呼び出すことで引数を捕まえ、captor.getValue()で取得して詳細なアサーションを行っています。

Spy: 実オブジェクトの一部をモック化

@Spyまたはspy()メソッドを使うと、実オブジェクトをラップしたオブジェクトを作成できます。デフォルトでは、Spyオブジェクトのメソッドを呼び出すと本物のメソッドが実行されます

Spyの主な目的は、実オブジェクトの挙動を使いつつ、一部のメソッドだけをスタブ化(置き換え)することです。レガシーコードで巨大なクラスをテストする際に、一部の外部依存メソッドだけをモックにしたい、といった場合に有効です。

Spyの注意点: Spyのメソッドをスタブ化する際は、when(spy.method()).thenReturn(...)の構文を使うと、when()の内部で本物のメソッドが呼び出されてしまうため、意図しない挙動になることがあります。代わりに、doReturn(value).when(spy).method() の構文を使用することが強く推奨されます。この構文は本物のメソッドを呼び出しません。

class RealList extends ArrayList<String> {
    // 巨大なクラスの一部だと思ってください
}

@Test
void testSpy() {
    // 実オブジェクトをスパイ化
    RealList spyList = spy(new RealList());

    // 1. 本物のメソッドを呼び出す
    spyList.add("one");
    spyList.add("two");
    verify(spyList).add("one");
    verify(spyList).add("two");
    assertEquals(2, spyList.size()); // 本物のsize()が呼ばれる

    // 2. 一部のメソッドをスタブ化 (推奨される構文)
    doReturn(100).when(spyList).size();

    // スタブ化したメソッドは定義した値を返す
    assertEquals(100, spyList.size());
}

Spyは便利ですが、テスト設計が複雑になる傾向があるため、多用は避けるべきです。可能な限り、依存性の注入(DI)によってモック可能な設計を目指すのが理想です。

BDD (振る舞い駆動開発) スタイル

Mockitoは、BDDの “Given-When-Then” スタイルに合わせたエイリアスメソッドを提供しています。これにより、テストコードがより自然言語に近くなり、可読性が向上します。

  • given(mock.method()).willReturn(value)when().thenReturn() のエイリアスです。
  • then(mock).should().method()verify(mock) のエイリアスです。
import static org.mockito.BDDMockito.*;

@Test
void bddStyleTest() {
    // Given (前提条件)
    UserService mockUserService = mock(UserService.class);
    given(mockUserService.getUserName(1)).willReturn("BDD User");

    Greeter greeter = new Greeter(mockUserService);

    // When (操作)
    String result = greeter.greet(1);

    // Then (結果の検証)
    assertEquals("Hello, BDD User!", result);
    then(mockUserService).should(times(1)).getUserName(1);
    then(mockUserService).shouldHaveNoMoreInteractions(); // 他のメソッド呼び出しがないことを検証
}

機能的には通常スタイルと全く同じですが、チームのコーディング規約や好みに応じて採用を検討すると良いでしょう。

第8章: ベストプラクティスと注意点

最後に、Mockitoを効果的に使うためのベストプラクティスと、初心者が陥りがちな注意点をいくつか紹介します。

  • 1. 状態のテストを優先し、振る舞いの検証は控えめに: verify()を多用すると、テストが実装の詳細に密結合し、リファクタリングで壊れやすくなります(脆いテスト)。SUTのメソッドを呼び出した結果、状態(戻り値やオブジェクトの内部状態)がどう変化したかをアサートすることを主眼に置き、verify()は「副作用として外部コンポーネントが正しく呼ばれたか」を補助的に確認する程度に留めるのが良いでしょう。
  • 2. 1つのテストメソッドでは1つのことを検証する: 1つのテストメソッドに多くのスタブ化と検証を詰め込むと、テストが失敗した際に原因の特定が困難になります。テストメソッド名は「何を」「どういう状況で」テストするのかが明確にわかるようにし、関心事を分離しましょう。
  • 3. staticメソッドやfinalクラスのモック化: 歴史的に、Mockitoはstaticメソッド、finalクラス、privateメソッドなどのモック化を直接サポートしていませんでした。しかし、Mockito 3.4.0以降では、mockito-inlineというモジュールが導入され(Mockito 5以降はデフォルトで有効)、これらの要素もモック化できるようになりました。特別な設定なしにmock()finalクラスをモックしたり、Mockito.mockStatic()を使ってstaticメソッドの振る舞いを定義したりできます。ただし、これは強力すぎるため、テスト設計が困難なレガシーコードへの最終手段と考えるべきです。
  • // staticメソッドのモック化の例
    try (MockedStatic<LocalDateTime> mocked = mockStatic(LocalDateTime.class)) {
        LocalDateTime fixedTime = LocalDateTime.of(2025, 1, 1, 12, 0);
        mocked.when(LocalDateTime::now).thenReturn(fixedTime);
    
        // このブロック内では LocalDateTime.now() が固定値を返す
        assertEquals(fixedTime, LocalDateTime.now());
    }
    
    // ブロックを抜けると元の挙動に戻る
    assertNotEquals(LocalDateTime.of(2025, 1, 1, 12, 0), LocalDateTime.now());
    
  • 4. モックの乱用を避ける: 何でもかんでもモックにするのは良いプラクティスではありません。値オブジェクト(Value Object)やDTOのような、ロジックを持たず状態を保持するだけのシンプルなオブジェクトは、モック化せずに実インスタンスを使った方がテストがシンプルで分かりやすくなります。モックは、テストを困難にする「依存」を断ち切るために使いましょう。

まとめ

この記事では、Javaの単体テストに不可欠なモッキングフレームワークであるMockitoについて、その基本から応用までを網羅的に解説しました。

モックオブジェクトの作成、振る舞いの設定(スタブ化)、そして呼び出しの検証という3つの基本ステップから始まり、アノテーションによる効率化、ArgumentCaptorSpyといった高度な機能までを学びました。

Mockitoを正しく使いこなすことで、テストは外部環境から独立し、高速かつ安定的に実行できるようになります。これにより、自信を持ってコードの改善や機能追加に取り組むことができ、ソフトウェア全体の品質向上に大きく貢献します。

最初は覚えることが多いと感じるかもしれませんが、まずは基本的なmock(), when().thenReturn(), verify()から始めてみてください。実践を通じて、Mockitoがいかにテスト開発を快適で効率的なものにしてくれるかを実感できるはずです。

コメントを残す

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