前回、単体テストの基本と、その原則が「外部への依存関係を排除し、テスト対象のコード単独の動作を検証すること」にあることを学びました。
しかし、実際のアプリケーションのクラス(Serviceなど)は、データベースアクセスを行うRepositoryや外部APIクライアントに必ず依存しています。純粋な単体テストを実現するためには、これらの依存関係を本物そっくりな偽物に置き換える技術が必要です。
今回は、この「偽物」を実現するための**モック(Mock)とスタブ(Stub)**という概念、そしてJavaで最も広く使われているモックライブラリ Mockito の基本的な使い方を学びます。
1. モックとスタブの概念
テストで依存関係の偽物として使われるものを総称して**テストダブル(Test Double)**と呼びます。その中でも、特に重要なのが「スタブ」と「モック」です。
1-1. スタブ(Stub)
- 役割: 呼び出されたときに、あらかじめ準備しておいた**決まった値(スタブ値)**を返すだけのシンプルな偽物です。
- 目的: テスト対象のメソッドが、依存先の戻り値(例:Repositoryの検索結果)を必要とする場合に、その戻り値を提供する。
1-2. モック(Mock)
- 役割: スタブの機能に加え、テスト対象のメソッドが依存先の**メソッドを「正しく呼び出したかどうか」**を検証するための偽物です。
- 目的: テスト対象のメソッドが、依存先に対して状態変更を伴うメソッド(例:
save()やdelete())を「意図した通りに、意図した引数で」呼び出したことを検証する。
2. Mockitoの基本的な使い方
Mockitoは、Javaでモックやスタブを簡単に作成・管理するためのデファクトスタンダードなライブラリです。
2-1. 準備:テストクラスへのアノテーション
Spring Bootのテストでは、@ExtendWith(MockitoExtension.class) を使用してMockitoの拡張機能を有効にします。
Java
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class ItemServiceTest {
// ...
}
2-2. モックの作成と注入
テスト対象のクラスとその依存関係を、それぞれ @InjectMocks と @Mock アノテーションで宣言します。
Java
import org.mockito.InjectMocks;
import org.mockito.Mock;
@ExtendWith(MockitoExtension.class)
class ItemServiceTest {
// ① テスト対象のServiceクラス(モックを注入される側)
@InjectMocks
private ItemService itemService;
// ② 依存先のRepositoryクラス(偽物/モックにする側)
@Mock
private ItemRepository itemRepository;
// ... テストメソッド
}
2-3. スタブ(戻り値の定義)の作り方
スタブの動作(特定のメソッドが呼ばれたときに、何を返すか)は、when().thenReturn() 構文で定義します。
Java
import static org.mockito.Mockito.when;
@Test
@DisplayName("在庫数を確認して、注文が可能なこと")
void testOrder_AvailableStock() {
Long itemId = 1L;
// スタブ設定: itemRepository.findById(1L) が呼ばれたら、在庫10のItemを返す
when(itemRepository.findById(itemId))
.thenReturn(Optional.of(new Item(itemId, "Test Item", 10)));
// 実行
boolean canOrder = itemService.checkStock(itemId, 5);
// 検証
assertTrue(canOrder);
}
2-4. モック(呼び出しの検証)の使い方
テスト対象のメソッドが、依存先のメソッドを正しく呼び出したかどうかを検証するには、verify() 構文を使います。これは、状態変更を伴うメソッド(保存、削除など)のテストで特に重要です。
Java
import static org.mockito.Mockito.verify;
@Test
@DisplayName("在庫確認後、在庫数が正しく更新されること")
void testUpdateStock_Success() {
Long itemId = 1L;
Item mockItem = new Item(itemId, "Test Item", 10);
// スタブ: 検索結果を返す
when(itemRepository.findById(itemId)).thenReturn(Optional.of(mockItem));
// 実行: 5つ注文
itemService.orderItem(itemId, 5);
// 検証: itemRepository.save() が、在庫が5に更新されたItemオブジェクトを引数にして
// ちょうど1回だけ呼び出されたことを検証
verify(itemRepository, times(1)).save(mockItem);
assertEquals(5, mockItem.getStock());
}
3. モックと単体テストのメリット
Mockitoを使うことで、以下のメリットが得られます。
- 高速性: DBアクセスやネットワーク通信が発生しないため、テストが非常に高速に実行されます。
- 再現性: 外部環境に依存しないため、いつ、どこで実行しても同じ結果が得られます(安定性)。
- エラーケースの検証: 「DBアクセスが失敗した場合」「外部APIがエラーを返した場合」など、通常では再現が難しいエラーケースをモックを使って意図的に作り出すことができます。
✅ 本日のまとめ
- テストダブルは、単体テストで依存関係を置き換えるための偽物である。
- **スタブ(Stub)**は、決まった値を返して、戻り値を提供する偽物である。
- **モック(Mock)**は、戻り値を提供する機能に加え、メソッドの呼び出し履歴を検証するための偽物である。
- Mockitoは、Javaの標準的なモックライブラリであり、
@Mock、@InjectMocksでモックの作成と注入を行う。 - スタブは
when().thenReturn()、呼び出し検証はverify()構文で行う。
🔔 次回予告
単体テストの次は、Springの機能が正しく動作するかを含めた、より統合的なテストに進みます。
次回は、SpringのDIコンテナを起動し、Web層(Controller)とService層など、複数の層を結合して検証する**結合テスト(Integration Test)**の書き方を学びます。
次回:【第40回】複数層をまとめて検証!〜結合テスト(Integration Test)の基本〜 にご期待ください!


コメント