Mockito
単体テストツール
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 クラスやメソッドのモック化がデフォルトで可能になっています。
メリット・デメリット
メリット
- 学習コストの低さ: 直感的なAPIで初心者でも習得しやすい
- 豊富なドキュメント: 公式ドキュメントとサンプルが充実
- 優れた統合性: JUnit、TestNG、Spring Bootとシームレスに統合
- 詳細なエラーメッセージ: 失敗時に分かりやすいエラー情報を提供
- 高いパフォーマンス: 軽量で高速なテスト実行
- 活発なコミュニティ: 大規模なユーザーコミュニティと継続的な開発
- BDD対応: BDDMockitoによる行動駆動開発スタイルのサポート
デメリット
- 過度のモック化リスク: モックに依存しすぎると実際の統合問題を見逃す可能性
- Java言語限定: 他の言語では使用不可
- 学習曲線: 高度な機能(カスタムアンサー、引数キャプター等)は習得に時間が必要
- デバッグの困難さ: 複雑なモック設定時のデバッグが困難
- 実装に密結合: モックが実装詳細に依存する場合がある
参考ページ
使い方の例
基本セットアップ
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();