Mockito

Java単体テストモッキングBDDTDDJUnitTestNG検証スタブ

単体テストツール

Mockito

概要

Mockitoは、Java用の最も人気の高いモッキングフレームワークです。テスト駆動開発(TDD)や行動駆動開発(BDD)において、テストダブルオブジェクト(モックオブジェクト)を作成し、オブジェクト間の相互作用を検証するために使用されます。美しく読みやすいテストをクリーンでシンプルなAPIで書くことができる、まさに「美味しい」モッキングフレームワークです。

詳細

Mockitoは、元々はEasyMockやJMockなどの既存のモッキングフレームワークの欠点を改善するために開発されました。現在では30,000以上のGitHubプロジェクトで使用され、StackOverflowコミュニティからJava最高のモッキングフレームワークとして評価されています。2024年の最新バージョンは5.14.2で、Java 11以上が必要です。

Mockitoの主な特徴:

  • シンプルなAPI: 直感的で覚えやすいAPIデザイン
  • 強力な検証機能: メソッド呼び出しの回数、順序、引数を詳細に検証
  • 柔軟なスタブ機能: メソッドの戻り値や例外を自由に設定
  • 引数マッチャー: 柔軟な引数マッチングによる検証
  • スパイ機能: 実際のオブジェクトを部分的にモック化
  • 静的メソッドモック: 静的メソッドの呼び出しをモック化(mockito-inline使用)
  • コンストラクタモック: クラスのインスタンス化をモック化

Mockito 5では、デフォルトのmockmakeがmockito-inlineに変更され、Java 11が必須となりました。これにより、final クラスやメソッドのモック化がデフォルトで可能になっています。

メリット・デメリット

メリット

  1. 学習コストの低さ: 直感的なAPIで初心者でも習得しやすい
  2. 豊富なドキュメント: 公式ドキュメントとサンプルが充実
  3. 優れた統合性: JUnit、TestNG、Spring Bootとシームレスに統合
  4. 詳細なエラーメッセージ: 失敗時に分かりやすいエラー情報を提供
  5. 高いパフォーマンス: 軽量で高速なテスト実行
  6. 活発なコミュニティ: 大規模なユーザーコミュニティと継続的な開発
  7. BDD対応: BDDMockitoによる行動駆動開発スタイルのサポート

デメリット

  1. 過度のモック化リスク: モックに依存しすぎると実際の統合問題を見逃す可能性
  2. Java言語限定: 他の言語では使用不可
  3. 学習曲線: 高度な機能(カスタムアンサー、引数キャプター等)は習得に時間が必要
  4. デバッグの困難さ: 複雑なモック設定時のデバッグが困難
  5. 実装に密結合: モックが実装詳細に依存する場合がある

参考ページ

使い方の例

基本セットアップ

Maven依存関係

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

JUnit 5との統合

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    
    @Mock
    private UserRepository userRepository;
    
    @Test
    void shouldCreateUser() {
        // テスト実装
    }
}

モック作成とスタブ

基本的なモック作成

// モック作成
List<String> mockedList = mock(List.class);

// スタブ設定
when(mockedList.get(0)).thenReturn("first");
when(mockedList.size()).thenReturn(5);

// 使用
System.out.println(mockedList.get(0)); // "first"
System.out.println(mockedList.size()); // 5

例外のスタブ

when(mockedList.get(anyInt())).thenThrow(new RuntimeException());

// void メソッドの例外
doThrow(new RuntimeException()).when(mockedList).clear();

検証(Verification)

基本的な検証

// メソッド呼び出しの検証
verify(mockedList).add("item");
verify(mockedList, times(1)).clear();
verify(mockedList, never()).remove("item");
verify(mockedList, atLeastOnce()).size();
verify(mockedList, atMost(3)).get(anyInt());

呼び出し順序の検証

InOrder inOrder = inOrder(mockedList);
inOrder.verify(mockedList).add("first");
inOrder.verify(mockedList).add("second");

引数マッチャー

組み込みマッチャー

// 任意の引数
when(mockedList.get(anyInt())).thenReturn("element");

// 特定の型
when(userService.findUser(any(String.class))).thenReturn(user);

// 引数キャプター
ArgumentCaptor<String> argument = ArgumentCaptor.forClass(String.class);
verify(mockedList).add(argument.capture());
assertEquals("captured value", argument.getValue());

カスタムマッチャー

when(userService.authenticate(argThat(user -> 
    user.getEmail().endsWith("@company.com")
))).thenReturn(true);

スパイ(Spy)

部分モック

List<String> list = new ArrayList<>();
List<String> spyList = spy(list);

// 実際のメソッドが呼ばれる
spyList.add("one");
spyList.add("two");

// スタブも可能
when(spyList.size()).thenReturn(100);

assertEquals(100, spyList.size()); // スタブされた値
assertEquals("one", spyList.get(0)); // 実際の値

カスタムアンサー

複雑な動作の定義

when(userService.processUser(any(User.class))).thenAnswer(invocation -> {
    User user = invocation.getArgument(0);
    return "Processed: " + user.getName();
});

// Lambda式でのシンプルなアンサー
when(calculator.add(anyInt(), anyInt())).thenAnswer(invocation -> {
    int a = invocation.getArgument(0);
    int b = invocation.getArgument(1);
    return a + b;
});

BDD スタイル

BDDMockito使用

import static org.mockito.BDDMockito.*;

@Test
void shouldReturnUserWhenFound() {
    // Given
    User expectedUser = new User("John", "[email protected]");
    given(userRepository.findById(1L)).willReturn(expectedUser);
    
    // When
    User actualUser = userService.getUser(1L);
    
    // Then
    then(userRepository).should().findById(1L);
    assertThat(actualUser).isEqualTo(expectedUser);
}

静的メソッドとコンストラクタのモック

静的メソッドモック

@Test
void shouldMockStaticMethod() {
    try (MockedStatic<LocalDateTime> mockedStatic = mockStatic(LocalDateTime.class)) {
        LocalDateTime fixedTime = LocalDateTime.of(2024, 1, 1, 12, 0);
        mockedStatic.when(LocalDateTime::now).thenReturn(fixedTime);
        
        assertEquals(fixedTime, LocalDateTime.now());
    }
}

コンストラクタモック

@Test
void shouldMockConstruction() {
    try (MockedConstruction<File> mockedConstruction = mockConstruction(File.class)) {
        File file = new File("test.txt");
        
        verify(mockedConstruction.constructed().get(0)).exists();
    }
}

高度な検証

厳密なスタブ

// 厳密モード(不要なスタブを検出)
MockitoSession mockito = Mockito.mockitoSession()
    .initMocks(this)
    .strictness(Strictness.STRICT_STUBS)
    .startMocking();

// 必要に応じて寛容モードに変更
lenient().when(mockedList.get(0)).thenReturn("element");

タイムアウト検証

// 一定時間内の呼び出しを検証
verify(mockedList, timeout(1000)).add("item");
verify(mockedList, timeout(1000).times(2)).clear();