JavaでMockitoを使った依存性注入とスパイの具体的な利用方法を徹底解説

Mockitoは、Javaにおけるテストフレームワークの一つであり、特に依存関係を扱う際に非常に便利です。大規模なアプリケーションでは、オブジェクト同士が密接に結びついており、その依存関係を適切にテストすることが難しくなります。Mockitoを使用することで、テスト中に不要な依存関係を「モック」や「スパイ」という手法を使って管理し、効率的にテストを実施できます。

本記事では、Mockitoを用いた依存性注入(Dependency Injection)とスパイ(Spy)の具体的な使い方について、実践的なコード例を交えて解説します。特に、依存性を管理しながらテストコードを簡潔に保つ方法や、スパイを使った部分的なオブジェクト監視のテクニックについて詳しく学んでいきます。

目次
  1. Mockitoとは何か
    1. モックとスパイの違い
  2. 依存性注入の基礎概念
    1. 依存性注入の利点
    2. 依存性注入の種類
  3. Mockitoでの依存性注入の設定方法
    1. コンストラクタインジェクションを用いた依存性注入
    2. セッターインジェクションを用いた依存性注入
    3. フィールドインジェクションを用いた依存性注入
  4. スパイとモックの違い
    1. モック(Mock)の特徴
    2. スパイ(Spy)の特徴
    3. モックとスパイの使い分け
  5. Mockitoでスパイを使用する方法
    1. スパイの基本的な使い方
    2. スパイを使用する際の注意点
    3. スパイを利用したテストの実例
  6. 依存性注入を使ったテストケースの実装例
    1. 依存性注入の基本的なテスト例
    2. 依存性注入を使ったテストコード
    3. セッターインジェクションを使ったテスト例
    4. フィールドインジェクションを使ったテスト例
  7. スパイを使ったテストケースの実装例
    1. スパイを使った部分的なオブジェクト操作の例
    2. スパイを使ったテストの実装
    3. テストの流れ
    4. スパイを使用する際の注意点
  8. 依存性注入とスパイの組み合わせ
    1. 依存性注入とスパイの連携の利点
    2. 実際のテストケース:依存性注入とスパイの組み合わせ
    3. テストコードの実装
    4. このテストケースの解説
    5. 依存性注入とスパイを組み合わせるシチュエーション
  9. よくあるエラーとその対処法
    1. 1. NullPointerExceptionが発生する
    2. 2. `when(…).thenReturn(…)`が効かない
    3. 3. `UnfinishedStubbingException`が発生する
    4. 4. `TooManyActualInvocationsException`が発生する
    5. 5. `ArgumentCaptor`が正しく機能しない
    6. 6. `MockCreationException`が発生する
  10. 演習問題
    1. 演習1: コンストラクタインジェクションを使ったモックのテスト
    2. 演習2: セッターインジェクションを使ったスパイのテスト
    3. 演習3: スパイとモックの併用
  11. まとめ

Mockitoとは何か

Mockitoは、Javaにおける人気の高いモックフレームワークで、主に単体テストで使用されます。モックオブジェクトを使って依存関係を模擬し、テスト対象のコードが期待通りに機能するかどうかを検証できます。これは、実際の外部リソース(データベース、Webサービスなど)に依存しないテストを可能にし、テストの速度と信頼性を高めるために役立ちます。

モックとスパイの違い

Mockitoには、モック(Mock)スパイ(Spy)の2つの主要な手法があります。モックは、依存するオブジェクトの完全な模倣を作り、すべてのメソッドをカスタマイズ可能なダミーオブジェクトに置き換えます。一方、スパイは実際のオブジェクトを部分的に置き換え、特定のメソッドだけを模擬的に扱うことができます。

  • モック: 完全に依存オブジェクトを置き換える。
  • スパイ: 一部のメソッドは実行し、一部はモックする。

Mockitoを使用することで、依存するオブジェクトの挙動を細かく制御できるため、テスト環境での予測不可能な動作を回避し、テストの信頼性を高めることができます。

依存性注入の基礎概念

依存性注入(Dependency Injection)とは、オブジェクト指向プログラミングにおけるデザインパターンの一つで、クラス間の依存関係を外部から注入することで、コードの柔軟性とテストの容易さを向上させる技術です。通常、オブジェクトAがオブジェクトBに依存する場合、Aが直接Bを生成・管理するのではなく、Bを外部から提供(注入)することで、依存性を緩和し、Aのテストを独立して行えるようにします。

依存性注入の利点

依存性注入には、以下の利点があります。

テストの容易さ

依存関係が外部から注入されるため、テスト時にモックやスパイを利用して、依存するオブジェクトの挙動を制御しやすくなります。これにより、特定のメソッドやクラスの単体テストが容易に行えます。

コードの再利用性と柔軟性

依存関係がコード内で固定されていないため、異なる依存関係を簡単に差し替えることができ、コードの再利用性が向上します。また、新しい機能追加や仕様変更時も、既存のコードに影響を与えずに依存関係を変更できるため、柔軟なアーキテクチャ設計が可能になります。

依存性注入の種類

依存性注入には、主に3つの種類があります。

  • コンストラクタインジェクション: コンストラクタを介して依存関係を注入する方法。
  • セッターインジェクション: セッターメソッドを使って依存関係を注入する方法。
  • フィールドインジェクション: フィールドに直接依存関係を注入する方法。

これらの手法を適切に用いることで、テスト可能で保守しやすいコードを実現できます。

Mockitoでの依存性注入の設定方法

Mockitoを使用すると、依存性注入を活用してテスト環境を簡単に構築できます。これにより、外部依存関係をモック(偽のオブジェクト)に置き換えることで、テストを効率化し、依存関係による予期せぬ動作を防ぐことが可能です。ここでは、Mockitoを使った依存性注入の設定方法について説明します。

コンストラクタインジェクションを用いた依存性注入

コンストラクタインジェクションでは、テスト対象のクラスに依存するオブジェクトをコンストラクタ経由で注入します。以下はその具体例です。

class Service {
    private final Repository repository;

    public Service(Repository repository) {
        this.repository = repository;
    }

    public String fetchData() {
        return repository.getData();
    }
}

class Repository {
    public String getData() {
        return "Real Data";
    }
}

ここで、ServiceRepositoryに依存しています。テストでは、このRepositoryをモックに置き換えることができます。

@Test
public void testFetchData() {
    // Repositoryをモック化
    Repository mockRepository = Mockito.mock(Repository.class);
    // モックの挙動を定義
    Mockito.when(mockRepository.getData()).thenReturn("Mock Data");

    // モックをServiceに注入
    Service service = new Service(mockRepository);

    // テストの実行とアサーション
    assertEquals("Mock Data", service.fetchData());
}

このコードでは、Repositoryがモック化され、Serviceはテスト用のデータを使用して動作するため、依存関係に左右されないテストが可能になります。

セッターインジェクションを用いた依存性注入

セッターインジェクションでは、依存オブジェクトをセッターメソッドを通して注入します。以下はその例です。

class Service {
    private Repository repository;

    public void setRepository(Repository repository) {
        this.repository = repository;
    }

    public String fetchData() {
        return repository.getData();
    }
}

この場合も、モックを使用して依存関係を注入することが可能です。

@Test
public void testFetchDataWithSetterInjection() {
    // Repositoryをモック化
    Repository mockRepository = Mockito.mock(Repository.class);
    Mockito.when(mockRepository.getData()).thenReturn("Mock Data");

    // モックをセット
    Service service = new Service();
    service.setRepository(mockRepository);

    // テストの実行とアサーション
    assertEquals("Mock Data", service.fetchData());
}

セッターインジェクションは、依存関係を後から設定する柔軟性を提供します。

フィールドインジェクションを用いた依存性注入

フィールドインジェクションでは、フィールドに直接依存関係を注入します。Mockitoの@InjectMocksアノテーションを使用すると、フィールドインジェクションによる依存性注入が簡単に行えます。

class Service {
    @InjectMocks
    private Repository repository;

    public String fetchData() {
        return repository.getData();
    }
}

Mockitoを使えば、これらの異なる注入方法で柔軟に依存性を管理でき、テスト環境を整えることが容易になります。

スパイとモックの違い

Mockitoを使用する際に、モック(Mock)とスパイ(Spy)の使い分けが非常に重要です。それぞれの役割や用途に応じて選択することで、より効果的なテストを行うことができます。ここでは、スパイとモックの違いについて詳しく解説します。

モック(Mock)の特徴

モックは、完全にテスト用に作成されたダミーオブジェクトです。実際のオブジェクトの代わりに、メソッド呼び出しや動作をシミュレーションし、テスト対象のクラスがその依存オブジェクトとどのようにやりとりするかを検証するために使用されます。

  • 完全にテスト用のオブジェクト: 依存オブジェクトを全て模倣し、実際の実装とは無関係のダミーオブジェクトを使用します。
  • カスタマイズ可能: 特定のメソッドがどのような動作をするか、事前に期待値や戻り値を設定できます。
  • パフォーマンスの向上: 実際のオブジェクトを使用せずに済むため、テストの速度が向上します。
Repository mockRepository = Mockito.mock(Repository.class);
Mockito.when(mockRepository.getData()).thenReturn("Mock Data");

このようにモックは、完全にコントロールされた依存関係を作り出し、テスト対象が正しく動作しているかを確認できます。

スパイ(Spy)の特徴

スパイは、実際のオブジェクトを部分的にモック化する手法です。実際のオブジェクトの挙動をそのまま利用しつつ、特定のメソッドだけをモックに差し替えて、挙動を監視・制御することができます。

  • 部分的な模擬: 実際のオブジェクトの挙動を保持しながら、特定のメソッドだけをモック化します。
  • 実際のメソッド呼び出しが可能: 実際のメソッドをそのまま呼び出し、必要に応じてモック化されたメソッドと組み合わせることができます。
  • 既存コードの確認に最適: 既存のロジックが期待通りに動作しているかどうかを確認しながら、一部の動作のみを制御する場合に便利です。
Repository spyRepository = Mockito.spy(new Repository());
Mockito.doReturn("Spy Data").when(spyRepository).getData();

スパイでは、このように実際のオブジェクトを作成した上で、一部のメソッドのみをモック化し、特定の動作を監視します。

モックとスパイの使い分け

  • モックは、依存関係を完全にコントロールしたい場合、特に依存オブジェクトの実装に影響されず、テストしたいときに適しています。
  • スパイは、実際のオブジェクトの挙動を確認しつつ、特定の動作のみをモック化してテストを行いたい場合に適しています。

例えば、外部システムと通信するクラスをモックにする場合、外部システムに依存せずに挙動を完全にシミュレーションできるモックが適しています。一方、既存のロジックの一部を変更して挙動をテストしたい場合はスパイが有効です。

Mockitoでスパイを使用する方法

Mockitoでスパイ(Spy)を使用すると、実際のオブジェクトを利用しつつ、その一部のメソッドだけをモック化して挙動を制御できます。スパイは、モックと異なり、基本的に実際のメソッド呼び出しが行われますが、特定のメソッドだけを監視したり、返り値を変更したりする際に便利です。

スパイの基本的な使い方

スパイを作成するには、Mockito.spy()メソッドを使用します。これにより、指定したオブジェクトがスパイ化され、通常通り動作しながら、一部のメソッドだけをカスタマイズすることができます。

// 実際のオブジェクトをスパイとして作成
Repository repository = new Repository();
Repository spyRepository = Mockito.spy(repository);

// スパイしたオブジェクトに特定の動作を設定
Mockito.doReturn("Spy Data").when(spyRepository).getData();

// テスト実行
assertEquals("Spy Data", spyRepository.getData());

この例では、Repositoryオブジェクトをスパイとして作成し、getData()メソッドの挙動を変更しています。それ以外のメソッドは、元のRepositoryオブジェクトのままの挙動を保持します。

スパイを使用する際の注意点

スパイは便利ですが、いくつかの注意点があります。

実際のメソッドが呼び出される

スパイでは、モックとは異なり、指定しなかったメソッドは実際に呼び出されます。つまり、依存関係が多い場合や、メソッドが外部サービスに接続している場合には、予期せぬ動作が発生する可能性があります。不要なメソッド呼び出しを抑制するためには、適切にモック化するか、doReturn()を使って特定のメソッドをオーバーライドする必要があります。

`doReturn()`と`when()`の使い分け

スパイを使う際には、doReturn()を使用するのが一般的です。通常のモックではwhen()を使用しますが、スパイの場合、doReturn()を用いることで、スパイオブジェクトのメソッドを確実に上書きできます。

// 正しい使い方:スパイでのメソッド上書き
Mockito.doReturn("Overridden Data").when(spyRepository).getData();

when()を使うとエラーが発生する場合があるため、スパイではdoReturn()を優先して使用します。

スパイを利用したテストの実例

次に、スパイを使った実際のテストケースを紹介します。この例では、ServiceクラスがRepositoryに依存しており、Repositoryの一部のメソッドのみをモック化してテストを行います。

public class Service {
    private final Repository repository;

    public Service(Repository repository) {
        this.repository = repository;
    }

    public String fetchData() {
        return repository.getData();
    }

    public String processData() {
        return "Processed: " + repository.getData();
    }
}

@Test
public void testProcessDataWithSpy() {
    // 実際のRepositoryオブジェクトをスパイ化
    Repository realRepository = new Repository();
    Repository spyRepository = Mockito.spy(realRepository);

    // getData()メソッドのみモック化
    Mockito.doReturn("Mock Data").when(spyRepository).getData();

    // スパイ化したRepositoryをServiceに注入
    Service service = new Service(spyRepository);

    // processData()メソッドのテスト
    assertEquals("Processed: Mock Data", service.processData());
}

このテストでは、processData()メソッド内でgetData()が呼び出されますが、スパイ化されたRepositoryによってMock Dataが返されます。これにより、Repositoryの他の部分の動作はそのまま保持しつつ、特定のメソッドだけをカスタマイズしたテストを行うことができます。

スパイは、実際のオブジェクトの動作を部分的にモック化することで、テストの柔軟性を保ちながら精度の高い検証を行うための有効な手段です。

依存性注入を使ったテストケースの実装例

依存性注入を用いることで、テスト対象のクラスが外部の依存関係に強く依存せず、柔軟かつ効率的なテストが可能となります。Mockitoを使った依存性注入の実装例を通じて、テストの仕組みを理解しましょう。

依存性注入の基本的なテスト例

ここでは、ServiceクラスがRepositoryという依存オブジェクトを使用してデータを取得するシナリオを考えます。Repositoryはデータベースや外部APIとの接続を扱うため、テストではその実装をモックに置き換えることで、外部環境に依存しないテストを行います。

まず、テスト対象となるクラスを定義します。

// 依存オブジェクト(Repository)
public class Repository {
    public String getData() {
        return "Real Data";
    }
}

// テスト対象のServiceクラス
public class Service {
    private final Repository repository;

    // コンストラクタによる依存性注入
    public Service(Repository repository) {
        this.repository = repository;
    }

    // 依存オブジェクトからデータを取得するメソッド
    public String fetchData() {
        return repository.getData();
    }
}

このServiceクラスは、Repositoryからデータを取得する依存関係を持っています。テストでは、Repositoryをモック化し、テスト用のデータを注入します。

依存性注入を使ったテストコード

次に、Mockitoを使ってRepositoryのモックを作成し、依存性注入を使ってServiceクラスの動作をテストします。

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

public class ServiceTest {

    @Test
    public void testFetchData() {
        // Repositoryのモックオブジェクトを作成
        Repository mockRepository = Mockito.mock(Repository.class);

        // モックの挙動を定義(getData()の戻り値を設定)
        Mockito.when(mockRepository.getData()).thenReturn("Mock Data");

        // モックをServiceに依存性注入
        Service service = new Service(mockRepository);

        // fetchData()メソッドのテスト
        String result = service.fetchData();

        // アサーション
        assertEquals("Mock Data", result);
    }
}

このテストケースの流れを簡単に説明します。

  1. モックオブジェクトの作成: Mockito.mock()メソッドでRepositoryのモックを作成します。
  2. モックの挙動を定義: Mockito.when()を使って、getData()が呼ばれたときに"Mock Data"を返すように設定します。
  3. 依存性注入: Serviceクラスのコンストラクタに、作成したモックを渡して注入します。
  4. テスト実行とアサーション: service.fetchData()メソッドを呼び出し、モックから返されたデータが期待通りであるかをassertEqualsで検証します。

このように、依存性注入を活用することで、外部環境の影響を排除しつつ、内部ロジックの正確さを効率的にテストすることが可能です。

セッターインジェクションを使ったテスト例

次に、セッターインジェクションを使用して依存性を注入するテスト例を示します。

public class Service {
    private Repository repository;

    // セッターによる依存性注入
    public void setRepository(Repository repository) {
        this.repository = repository;
    }

    public String fetchData() {
        return repository.getData();
    }
}

@Test
public void testFetchDataWithSetter() {
    // Repositoryのモックを作成
    Repository mockRepository = Mockito.mock(Repository.class);
    Mockito.when(mockRepository.getData()).thenReturn("Mock Data");

    // Serviceオブジェクトを作成し、セッターでモックを注入
    Service service = new Service();
    service.setRepository(mockRepository);

    // fetchData()メソッドのテスト
    String result = service.fetchData();

    // アサーション
    assertEquals("Mock Data", result);
}

この例では、セッターメソッドを使って依存関係を注入しています。コンストラクタインジェクションと同様に、モックを利用してテストを行うことで、外部の実装に依存せずにテストケースを構築できます。

フィールドインジェクションを使ったテスト例

最後に、Mockitoの@InjectMocksアノテーションを使ってフィールドインジェクションを行う例です。

import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

public class ServiceTest {

    @Mock
    private Repository mockRepository;

    @InjectMocks
    private Service service;

    @BeforeEach
    public void init() {
        MockitoAnnotations.openMocks(this);
    }

    @Test
    public void testFetchDataWithFieldInjection() {
        // モックの挙動を定義
        Mockito.when(mockRepository.getData()).thenReturn("Mock Data");

        // fetchData()メソッドのテスト
        String result = service.fetchData();

        // アサーション
        assertEquals("Mock Data", result);
    }
}

この方法では、@Mockでモックを定義し、@InjectMocksでそのモックを自動的にServiceクラスに注入しています。これにより、フィールドインジェクションのテストも簡単に行えます。

以上が、依存性注入を使ったテストケースの実装例です。テストの種類に応じて適切な依存性注入の手法を選び、Mockitoを活用することで効率的かつ信頼性の高いテストが可能となります。

スパイを使ったテストケースの実装例

スパイ(Spy)は、モックと異なり、実際のオブジェクトをそのまま使用しながら、特定のメソッドだけをモック化するため、テストにおいて部分的な制御が必要な場面で非常に役立ちます。ここでは、スパイを使ったテストケースの具体的な実装例を紹介します。

スパイを使った部分的なオブジェクト操作の例

まず、ServiceクラスがRepositoryクラスに依存しているというシナリオを考えます。Repositoryクラスは複雑な処理を含んでいるかもしれませんが、今回はgetData()メソッドだけをモック化してテストします。

public class Repository {
    public String getData() {
        // 複雑なデータ処理
        return "Real Data";
    }

    public void saveData(String data) {
        // データを保存する処理
        System.out.println("Data saved: " + data);
    }
}

public class Service {
    private final Repository repository;

    public Service(Repository repository) {
        this.repository = repository;
    }

    public String fetchData() {
        return repository.getData();
    }

    public void processAndSaveData() {
        String data = repository.getData();
        repository.saveData("Processed: " + data);
    }
}

この例では、ServiceクラスのprocessAndSaveData()メソッドがRepositorygetData()saveData()メソッドを呼び出しています。getData()メソッドをモック化し、実際には呼び出さずに制御したいと考えますが、saveData()は実際に呼び出して確認したい場合にスパイを使用します。

スパイを使ったテストの実装

次に、Mockito.spy()を使ってスパイを作成し、部分的なモック化を行います。

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

public class ServiceTest {

    @Test
    public void testProcessAndSaveDataWithSpy() {
        // 実際のRepositoryオブジェクトを作成
        Repository realRepository = new Repository();

        // スパイとしてRepositoryをラップ
        Repository spyRepository = Mockito.spy(realRepository);

        // getData()メソッドのみモック化
        Mockito.doReturn("Mock Data").when(spyRepository).getData();

        // スパイ化したRepositoryをServiceに注入
        Service service = new Service(spyRepository);

        // processAndSaveData()メソッドのテスト
        service.processAndSaveData();

        // saveData()の実行結果を確認(実際に呼び出される)
        Mockito.verify(spyRepository).saveData("Processed: Mock Data");
    }
}

テストの流れ

  1. スパイの作成: Mockito.spy()を使って、Repositoryの実際のオブジェクトをスパイ化します。
  2. 部分的なモック化: Mockito.doReturn()を使って、getData()メソッドのみをモック化し、特定の返り値(ここでは”Mock Data”)を返すように設定します。
  3. 依存性注入: スパイ化したRepositoryServiceクラスに注入します。
  4. メソッドのテスト: processAndSaveData()メソッドを呼び出し、実際のsaveData()メソッドが期待通りに動作しているかを検証します。
  5. メソッド呼び出しの確認: Mockito.verify()を使って、saveData()メソッドが期待通りのパラメータで呼び出されたかどうかを確認します。

このテストケースでは、getData()メソッドのみをモック化し、他のメソッド(saveData())は実際の動作を検証しています。スパイを使用することで、既存のオブジェクトの動作をそのまま利用しつつ、特定の部分だけをコントロールできるため、実際のオブジェクトの振る舞いと部分的なモックを併用するテストが簡単に行えます。

スパイを使用する際の注意点

  • 実際のメソッドが呼び出される: スパイでは、明示的にモック化しない限り、実際のメソッドが呼び出されるため、依存オブジェクトが外部リソースにアクセスする場合は注意が必要です。
  • パフォーマンスへの影響: 実際のオブジェクトが使用されるため、モックのみを使う場合に比べてパフォーマンスが低下することがあります。スパイは、部分的にモックを行いたい場合にのみ使用し、モックで代替可能な場合はモックを優先すべきです。

以上が、スパイを使用したテストケースの実装例です。スパイをうまく活用することで、特定の挙動を制御しつつ、実際のオブジェクトの動作を検証するテストを実現できます。

依存性注入とスパイの組み合わせ

Mockitoを使った依存性注入とスパイの組み合わせは、テストをより柔軟で効率的にするための強力な手法です。このセクションでは、依存性注入とスパイを組み合わせた実践的なシナリオを紹介します。特に、外部依存関係を部分的にモック化しつつ、実際のオブジェクトの動作を残したテスト手法について見ていきます。

依存性注入とスパイの連携の利点

依存性注入とスパイを組み合わせることで、以下のような利点があります。

柔軟なテストの実現

依存性注入を使うことで、テスト中にオブジェクトの依存関係を簡単に差し替えられます。さらに、スパイを用いることで、テストしたい部分をコントロールしながら、実際のオブジェクトの動作を保持できます。これにより、テストが予期しない外部依存関係に左右されることなく、信頼性が高まります。

部分的なオブジェクトの監視

スパイを使えば、モック化したい部分だけをピンポイントで制御し、残りの部分は実際の動作をそのまま利用できます。これにより、依存オブジェクトが持つ実際の振る舞いも考慮に入れながら、特定のメソッドの挙動だけを変更することが可能です。

実際のテストケース:依存性注入とスパイの組み合わせ

次に、依存性注入とスパイを組み合わせたテストケースを紹介します。この例では、ServiceクラスがRepositoryに依存しており、Repositoryの一部のメソッドをモック化し、他のメソッドは実際に呼び出すシナリオを考えます。

public class Service {
    private final Repository repository;

    // コンストラクタによる依存性注入
    public Service(Repository repository) {
        this.repository = repository;
    }

    // データを取得するメソッド
    public String fetchData() {
        return repository.getData();
    }

    // データを処理して保存するメソッド
    public void processDataAndSave() {
        String data = repository.getData();
        repository.saveData("Processed: " + data);
    }
}

public class Repository {
    public String getData() {
        return "Real Data"; // 実際には複雑な処理が含まれる
    }

    public void saveData(String data) {
        // データを保存する(実際には外部リソースに書き込む処理)
        System.out.println("Data saved: " + data);
    }
}

ここでは、RepositorygetData()メソッドをモック化し、saveData()メソッドは実際に呼び出します。このような部分的なテストが必要なケースで、依存性注入とスパイを効果的に利用できます。

テストコードの実装

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

public class ServiceTest {

    @Test
    public void testProcessDataAndSaveWithSpy() {
        // 実際のRepositoryオブジェクトを作成
        Repository realRepository = new Repository();

        // スパイとしてRepositoryをラップ
        Repository spyRepository = Mockito.spy(realRepository);

        // getData()メソッドのみモック化
        Mockito.doReturn("Mock Data").when(spyRepository).getData();

        // Serviceにスパイ化したRepositoryを注入
        Service service = new Service(spyRepository);

        // processDataAndSave()メソッドをテスト
        service.processDataAndSave();

        // saveData()の実行結果を検証(実際に呼び出される)
        Mockito.verify(spyRepository).saveData("Processed: Mock Data");
    }
}

このテストケースの解説

  1. 依存性注入とスパイの組み合わせ: Repositoryオブジェクトをスパイ化し、Serviceクラスに依存性注入で渡します。これにより、テスト対象のクラス内で依存オブジェクトを自由に制御できます。
  2. 部分的なモック化: getData()メソッドだけをモック化し、"Mock Data"を返すようにします。その他のメソッドは、実際のRepositoryオブジェクトの実装に基づいて動作します。
  3. 実際のメソッド呼び出しの検証: saveData()メソッドは、実際のオブジェクトの動作に従って呼び出され、Mockito.verify()を使って正しい引数で呼び出されたことを確認します。

依存性注入とスパイを組み合わせるシチュエーション

依存性注入とスパイの組み合わせは、以下のような状況で特に有効です。

  • 外部APIやデータベースの一部だけをモック化したい場合: 外部のリソースとのやり取りの一部だけをモック化し、実際のデータ処理はそのままテストしたいとき。
  • 複雑な依存関係を持つオブジェクトの一部のメソッドを制御したい場合: 特定のメソッドの挙動だけを変更し、他のメソッドの動作を実際のオブジェクトで検証する必要があるとき。
  • 部分的な変更をテストしたい場合: 既存のコードの一部を変更した際、その変更箇所だけをモック化し、その他のロジックはそのままテストしたい場合。

このように、依存性注入とスパイを組み合わせることで、柔軟かつ効率的なテストケースを構築でき、特定の挙動を監視・制御しながら、実際のオブジェクトの動作も検証できます。

よくあるエラーとその対処法

Mockitoを使って依存性注入やスパイを利用する際に、いくつかの一般的なエラーや問題が発生することがあります。ここでは、そうしたよくあるエラーとその対処法について解説します。これらの問題に対処することで、テストの信頼性と効率を高めることができます。

1. NullPointerExceptionが発生する

原因: モックオブジェクトやスパイオブジェクトが適切に初期化されていない場合に発生します。特に、@InjectMocks@Mockを使用して依存性注入を行う際に、MockitoAnnotations.openMocks(this)が忘れられているケースがよくあります。

対処法: @Mock@InjectMocksを使用している場合、必ずテストクラスの初期化メソッドでMockitoAnnotations.openMocks(this)を呼び出し、Mockitoのアノテーションによるモックの生成を行いましょう。

@BeforeEach
public void initMocks() {
    MockitoAnnotations.openMocks(this);
}

この一文を@BeforeEachのメソッドに追加することで、モックオブジェクトが正しく初期化されます。

2. `when(…).thenReturn(…)`が効かない

原因: スパイを使用している場合、通常のwhen()メソッドが期待通りに動作しないことがあります。スパイでは、実際のオブジェクトを部分的にモック化するため、when()ではなくdoReturn()を使用する必要があります。

対処法: スパイを使用している場合、モック化するメソッドに対してはdoReturn()を使用します。

// スパイオブジェクトを使用する場合の正しい書き方
Mockito.doReturn("Mock Data").when(spyRepository).getData();

スパイでwhen()を使用すると、実際のメソッドが呼び出されてしまうため、必ずdoReturn()を使用するようにしてください。

3. `UnfinishedStubbingException`が発生する

原因: モック化したメソッドに対して、when()メソッドの設定が正しく完了していない場合に発生します。これは、when()の後にthenReturn()thenThrow()などの戻り値を指定する操作が行われていない場合に起こります。

対処法: when()を使う際は、必ず期待する動作を明示的に定義する必要があります。when()を呼んだ後に、thenReturn()thenThrow()で返り値を指定します。

// 不完全なモック設定
Mockito.when(mockRepository.getData());

// 完全なモック設定
Mockito.when(mockRepository.getData()).thenReturn("Mock Data");

必ず、when()の設定を完了させてからテストを実行しましょう。

4. `TooManyActualInvocationsException`が発生する

原因: モックオブジェクトやスパイオブジェクトに対して、期待されるよりも多くのメソッド呼び出しが行われた場合に発生します。たとえば、特定のメソッドが1回しか呼び出されるべきでないのに、複数回呼び出されているケースです。

対処法: テストの中で、どのメソッドが何回呼ばれているかを明確に検証し、過剰な呼び出しがないか確認しましょう。また、Mockito.verify()を使ってメソッドが期待通りに呼ばれているか確認することが重要です。

// メソッドの呼び出し回数を検証する
Mockito.verify(mockRepository, Mockito.times(1)).getData();

これにより、getData()メソッドが1回だけ呼び出されたことを確認できます。もし複数回呼び出す必要がある場合は、times()の引数を調整してください。

5. `ArgumentCaptor`が正しく機能しない

原因: MockitoのArgumentCaptorを使用する際、キャプチャ対象の引数が正しく取得できない場合があります。これは、キャプチャ対象のメソッドが期待通りに呼び出されていないか、キャプチャ対象がモック化されたメソッドの一部ではない可能性があります。

対処法: ArgumentCaptorを使用して引数をキャプチャする場合は、メソッドが確実に呼び出されていることを確認し、その呼び出しに対してキャプチャが行われることを保証します。

ArgumentCaptor<String> argumentCaptor = ArgumentCaptor.forClass(String.class);

// メソッド呼び出しをキャプチャ
Mockito.verify(mockRepository).saveData(argumentCaptor.capture());

// キャプチャした引数を検証
assertEquals("Processed: Mock Data", argumentCaptor.getValue());

これにより、saveData()メソッドで渡された引数が正しくキャプチャされ、検証できます。

6. `MockCreationException`が発生する

原因: モック化するクラスがfinalであったり、モックできない型(プリミティブ型や静的メソッドなど)を含んでいる場合に発生します。Mockitoは通常、finalクラスやfinalメソッドのモック化をサポートしていません。

対処法: finalクラスやメソッドをモック化する必要がある場合は、Mockitoの拡張機能を有効にするか、他の方法で依存関係をテストします。

// Mockitoでfinalクラスのモック化を有効にする
Mockito.mockStatic(FinalClass.class);

または、モック化が困難なクラスについては、設計を見直して依存性注入をより柔軟にする方法も検討してください。


これらの一般的なエラーを避けることで、Mockitoを使った依存性注入やスパイのテストをスムーズに行うことができ、テストケースの品質を向上させることができます。

演習問題

ここでは、Mockitoを使った依存性注入とスパイの知識を実践的に深めるための演習問題を提供します。これらの問題に取り組むことで、依存性の管理や部分的なモック化のテストスキルを強化できます。

演習1: コンストラクタインジェクションを使ったモックのテスト

問題: 以下のようなUserServiceクラスがあります。UserRepositoryをコンストラクタインジェクションで注入し、ユーザー情報を取得するメソッドgetUserInfo()をテストしてください。Mockitoを使ってUserRepositoryをモック化し、異なるシナリオ(成功時と失敗時)をテストしてください。

public class UserService {
    private final UserRepository userRepository;

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

    public String getUserInfo(int userId) {
        User user = userRepository.findUserById(userId);
        if (user == null) {
            return "User not found";
        }
        return "User info: " + user.getName();
    }
}

public class User {
    private int id;
    private String name;

    // コンストラクタ、getter、setter
}

ヒント:

  • Mockito.when()を使ってfindUserById()の戻り値を定義します。
  • userRepository.findUserById()nullを返す場合と、Userオブジェクトを返す場合の2つのシナリオをテストします。

解答例:

@Test
public void testGetUserInfo_UserFound() {
    UserRepository mockRepository = Mockito.mock(UserRepository.class);
    UserService userService = new UserService(mockRepository);

    User mockUser = new User(1, "John Doe");
    Mockito.when(mockRepository.findUserById(1)).thenReturn(mockUser);

    String result = userService.getUserInfo(1);
    assertEquals("User info: John Doe", result);
}

@Test
public void testGetUserInfo_UserNotFound() {
    UserRepository mockRepository = Mockito.mock(UserRepository.class);
    UserService userService = new UserService(mockRepository);

    Mockito.when(mockRepository.findUserById(1)).thenReturn(null);

    String result = userService.getUserInfo(1);
    assertEquals("User not found", result);
}

演習2: セッターインジェクションを使ったスパイのテスト

問題: 次のOrderServiceクラスでは、セッターインジェクションを使ってOrderRepositoryを注入しています。OrderRepositoryprocessOrder()メソッドをスパイを使ってモック化し、その動作を確認してください。validateOrder()メソッドは実際のオブジェクトの動作を維持する必要があります。

public class OrderService {
    private OrderRepository orderRepository;

    public void setOrderRepository(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    public boolean processOrder(int orderId) {
        if (orderRepository.validateOrder(orderId)) {
            return orderRepository.processOrder(orderId);
        }
        return false;
    }
}

ヒント:

  • スパイを使ってprocessOrder()メソッドをモック化し、validateOrder()は実際の動作を維持します。
  • Mockito.doReturn()を使用してprocessOrder()の戻り値を制御します。

解答例:

@Test
public void testProcessOrder_ValidOrder() {
    OrderRepository realRepository = new OrderRepository();
    OrderRepository spyRepository = Mockito.spy(realRepository);
    OrderService orderService = new OrderService();
    orderService.setOrderRepository(spyRepository);

    Mockito.doReturn(true).when(spyRepository).processOrder(1);

    boolean result = orderService.processOrder(1);
    assertTrue(result);

    // validateOrder()は実際に呼ばれる
    Mockito.verify(spyRepository).validateOrder(1);
}

@Test
public void testProcessOrder_InvalidOrder() {
    OrderRepository realRepository = new OrderRepository();
    OrderRepository spyRepository = Mockito.spy(realRepository);
    OrderService orderService = new OrderService();
    orderService.setOrderRepository(spyRepository);

    Mockito.doReturn(false).when(spyRepository).processOrder(1);
    Mockito.doReturn(false).when(spyRepository).validateOrder(1);

    boolean result = orderService.processOrder(1);
    assertFalse(result);

    // validateOrder()が呼ばれて、falseが返される
    Mockito.verify(spyRepository).validateOrder(1);
}

演習3: スパイとモックの併用

問題: 次のPaymentServiceクラスは、PaymentGatewayLoggerに依存しています。PaymentGatewayはスパイとして部分的にモック化し、Loggerは完全にモック化します。processPayment()メソッドをテストし、支払いが成功したときにLoggerが正しいメッセージを記録しているかを確認してください。

public class PaymentService {
    private PaymentGateway paymentGateway;
    private Logger logger;

    public PaymentService(PaymentGateway paymentGateway, Logger logger) {
        this.paymentGateway = paymentGateway;
        this.logger = logger;
    }

    public boolean processPayment(int amount) {
        if (paymentGateway.charge(amount)) {
            logger.log("Payment successful");
            return true;
        } else {
            logger.log("Payment failed");
            return false;
        }
    }
}

ヒント:

  • PaymentGatewayをスパイ化し、charge()メソッドのみモック化します。
  • Loggerは完全にモック化し、ログメッセージが正しく記録されるかをverify()で確認します。

解答例:

@Test
public void testProcessPayment_Successful() {
    PaymentGateway realGateway = new PaymentGateway();
    PaymentGateway spyGateway = Mockito.spy(realGateway);
    Logger mockLogger = Mockito.mock(Logger.class);

    PaymentService paymentService = new PaymentService(spyGateway, mockLogger);

    Mockito.doReturn(true).when(spyGateway).charge(100);

    boolean result = paymentService.processPayment(100);
    assertTrue(result);

    // ログが正しく記録されたか確認
    Mockito.verify(mockLogger).log("Payment successful");
}

@Test
public void testProcessPayment_Failed() {
    PaymentGateway realGateway = new PaymentGateway();
    PaymentGateway spyGateway = Mockito.spy(realGateway);
    Logger mockLogger = Mockito.mock(Logger.class);

    PaymentService paymentService = new PaymentService(spyGateway, mockLogger);

    Mockito.doReturn(false).when(spyGateway).charge(100);

    boolean result = paymentService.processPayment(100);
    assertFalse(result);

    // ログが正しく記録されたか確認
    Mockito.verify(mockLogger).log("Payment failed");
}

これらの演習問題を通じて、依存性注入やスパイ、モックを効果的に利用する方法を学ぶことができます。

まとめ

本記事では、JavaでのMockitoを使用した依存性注入とスパイの利用方法について解説しました。依存性注入を用いることで、テスト対象のクラスとその依存オブジェクトの結びつきを柔軟に管理でき、モックやスパイを活用することで、テストの精度と効率を向上させることができます。スパイを使うことで、実際のオブジェクトの動作を部分的に残しつつ、特定のメソッドだけを制御できるため、よりリアルなテストシナリオを実現できます。これらの技術を使いこなして、信頼性の高いユニットテストを構築しましょう。

コメント

コメントする

目次
  1. Mockitoとは何か
    1. モックとスパイの違い
  2. 依存性注入の基礎概念
    1. 依存性注入の利点
    2. 依存性注入の種類
  3. Mockitoでの依存性注入の設定方法
    1. コンストラクタインジェクションを用いた依存性注入
    2. セッターインジェクションを用いた依存性注入
    3. フィールドインジェクションを用いた依存性注入
  4. スパイとモックの違い
    1. モック(Mock)の特徴
    2. スパイ(Spy)の特徴
    3. モックとスパイの使い分け
  5. Mockitoでスパイを使用する方法
    1. スパイの基本的な使い方
    2. スパイを使用する際の注意点
    3. スパイを利用したテストの実例
  6. 依存性注入を使ったテストケースの実装例
    1. 依存性注入の基本的なテスト例
    2. 依存性注入を使ったテストコード
    3. セッターインジェクションを使ったテスト例
    4. フィールドインジェクションを使ったテスト例
  7. スパイを使ったテストケースの実装例
    1. スパイを使った部分的なオブジェクト操作の例
    2. スパイを使ったテストの実装
    3. テストの流れ
    4. スパイを使用する際の注意点
  8. 依存性注入とスパイの組み合わせ
    1. 依存性注入とスパイの連携の利点
    2. 実際のテストケース:依存性注入とスパイの組み合わせ
    3. テストコードの実装
    4. このテストケースの解説
    5. 依存性注入とスパイを組み合わせるシチュエーション
  9. よくあるエラーとその対処法
    1. 1. NullPointerExceptionが発生する
    2. 2. `when(…).thenReturn(…)`が効かない
    3. 3. `UnfinishedStubbingException`が発生する
    4. 4. `TooManyActualInvocationsException`が発生する
    5. 5. `ArgumentCaptor`が正しく機能しない
    6. 6. `MockCreationException`が発生する
  10. 演習問題
    1. 演習1: コンストラクタインジェクションを使ったモックのテスト
    2. 演習2: セッターインジェクションを使ったスパイのテスト
    3. 演習3: スパイとモックの併用
  11. まとめ