はじめに

Mockito は、Java アプリケーションの モックユニットテスト に使用される強力な Java フレームワークです。この実験(Lab)では、Mockito におけるスパイの概念を探求します。スパイは、部分的なモックとして機能するユニークなテストツールです。オブジェクトとのインタラクションを追跡しながら、実際のメソッドの呼び出しを許可します。

この二重の動作により、スパイはオブジェクトとのインタラクションを検証したいが、実際の機能を実行する必要がある場合に特に役立ちます。この実験を通して、さまざまなアプローチを使用してスパイを作成する方法、スパイの動作をスタブする方法を理解し、スパイとモックを比較して、その主な違いを明らかにします。

Java プロジェクトの作成と Mockito Spy の理解

このステップでは、プロジェクト構造を設定し、Mockito におけるスパイとは何かを理解します。スパイを使用すると、実際のオブジェクトのメソッド呼び出しを追跡しながら、その実際の実装を使用できます。

Java ファイルの作成

まず、この実験(Lab)のメイン Java ファイルを作成しましょう。WebIDE で、~/project ディレクトリに MockitoSpyDemo.java という新しいファイルを作成します。

新しいファイルの作成

必要な依存関係をインポートするために、次のコードを追加します。

import org.junit.Test;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.MockitoAnnotations;
import org.mockito.exceptions.misusing.NotAMockException;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.*;

public class MockitoSpyDemo {

    public static void main(String[] args) {
        System.out.println("This is a Mockito Spy demonstration.");
        System.out.println("To run the tests, you need to use JUnit.");
    }
}

Mockito スパイの理解

Mockito のスパイは、実際のオブジェクトをラップするもので、次のことができます。

  1. オブジェクトとのすべてのインタラクションを追跡する(モックのように)
  2. オブジェクトの実際のメソッドを実行する(モックとは異なり)

スパイを、オブジェクトに何が起こっているかを監視するオブザーバーとして考えてください。ただし、特別にプログラムされていない限り、通常の動作を妨げることはありません。

スパイの基本的な使用法を示す最初のテストメソッドを追加しましょう。次のメソッドを MockitoSpyDemo クラスに追加します。

@Test
public void basicSpyExample() {
    // Create a list and spy on it
    List<String> realList = new ArrayList<>();
    List<String> spyList = Mockito.spy(realList);

    // Using the spy to add an item (the real method is called)
    spyList.add("Hello");

    // Verify the interaction happened
    Mockito.verify(spyList).add("Hello");

    // The real method was called, so the list actually has the item
    assertEquals(1, spyList.size());
    assertEquals("Hello", spyList.get(0));

    System.out.println("Basic Spy Example - Spy List Content: " + spyList);
    System.out.println("Basic Spy Example - Spy List Size: " + spyList.size());
}

コードをコンパイルして実行し、動作を確認しましょう。

cd ~/project
javac -cp lib/junit.jar:lib/mockito-core.jar:lib/byte-buddy.jar:lib/byte-buddy-agent.jar:lib/objenesis.jar:lib/hamcrest-core.jar MockitoSpyDemo.java
java -cp .:lib/junit.jar:lib/mockito-core.jar:lib/byte-buddy.jar:lib/byte-buddy-agent.jar:lib/objenesis.jar:lib/hamcrest-core.jar MockitoSpyDemo
コードの実行

次の出力が表示されるはずです。

This is a Mockito Spy demonstration.
To run the tests, you need to use JUnit.

メインメソッドは正しく実行されます。ただし、テストを実行するには JUnit を使用する必要があります。この実験(Lab)のために複雑な JUnit ランナーを設定する代わりに、コード構造と概念の理解に焦点を当てます。

次のステップでは、Mockito でスパイを作成するさまざまな方法を探求します。

Mockito でのスパイ作成方法

このステップでは、Mockito でスパイを作成するためのさまざまなアプローチを探求します。Mockito.spy() メソッドと @Spy アノテーションを使用してスパイを作成する方法を学びます。

Mockito.spy() メソッドの使用

スパイを作成する最も直接的な方法は、Mockito.spy() メソッドを使用することです。このアプローチを示すテストメソッドを追加しましょう。

@Test
public void testSpyWithMockitoSpyMethod() {
    // Create an ArrayList and a spy of it
    ArrayList<Integer> realList = new ArrayList<>();
    ArrayList<Integer> spyList = Mockito.spy(realList);

    // Adding elements to the spy (real methods are called)
    spyList.add(5);
    spyList.add(10);
    spyList.add(15);

    // Verifying interactions
    Mockito.verify(spyList).add(5);
    Mockito.verify(spyList).add(10);
    Mockito.verify(spyList).add(15);

    // Verifying that elements were actually added to the list
    assertEquals(3, spyList.size());

    System.out.println("Spy Method Example - Spy List Content: " + spyList);
    System.out.println("Spy Method Example - Spy List Size: " + spyList.size());
}

@Spy アノテーションの使用

スパイを作成するもう一つの方法は、@Spy アノテーションを使用することです。このアプローチは、スパイを自動的に初期化したい場合に特に役立ちます。次のコードを MockitoSpyDemo クラスに追加します。

// Declare a spy using annotation
@Spy
ArrayList<Integer> annotationSpyList = new ArrayList<>();

@Before
public void initSpies() {
    // Initialize mocks and spies with annotations
    MockitoAnnotations.initMocks(this);
}

@Test
public void testSpyWithAnnotation() {
    // Adding elements to the spy
    annotationSpyList.add(5);
    annotationSpyList.add(10);
    annotationSpyList.add(15);

    // Verifying interactions
    Mockito.verify(annotationSpyList).add(5);
    Mockito.verify(annotationSpyList).add(10);
    Mockito.verify(annotationSpyList).add(15);

    // Verifying that elements were actually added to the list
    assertEquals(3, annotationSpyList.size());

    System.out.println("Annotation Spy Example - Spy List Content: " + annotationSpyList);
    System.out.println("Annotation Spy Example - Spy List Size: " + annotationSpyList.size());
}

アプローチの違いの理解

どちらのアプローチもスパイを作成しますが、異なるユースケースがあります。

  1. Mockito.spy() はより柔軟で、テストメソッドのどこでも使用できます。
  2. @Spy アノテーションは、多くのスパイがあり、それらを一度にすべて初期化したい場合に便利です。

MockitoAnnotations.initMocks(this) メソッドは、すべてのアノテーション付きフィールドを初期化する役割を担います。実際のプロジェクトでは、この初期化を自動的に行うために @RunWith(MockitoJUnitRunner.class) を使用する場合があります。

更新されたコードをコンパイルしましょう。

cd ~/project
javac -cp lib/junit.jar:lib/mockito-core.jar:lib/byte-buddy.jar:lib/byte-buddy-agent.jar:lib/objenesis.jar:lib/hamcrest-core.jar MockitoSpyDemo.java

コードはエラーなしでコンパイルされるはずです。テストメソッドはこれで準備完了しましたが、前述のように、それらを自動的に実行するには JUnit ランナーが必要になります。

次のステップでは、スパイのデフォルトの動作をオーバーライドするために、スパイをスタブする方法を学びます。

スパイのスタブ化と例外処理 (Mockito)

このステップでは、スパイのデフォルトの動作をオーバーライドするためにスパイをスタブする方法と、Mockito を使用する際に発生する可能性のある例外を処理する方法を学びます。

スパイのスタブ

スパイはデフォルトで実際のオブジェクトメソッドを使用しますが、テスト目的でこの動作を変更したい場合があります。これは、スパイの「スタブ化」と呼ばれます。スパイをスタブする方法を示すテストメソッドを追加しましょう。

@Test
public void testStubbingSpy() {
    // Create a spy of ArrayList
    ArrayList<Integer> spyList = Mockito.spy(new ArrayList<>());

    // Add an element
    spyList.add(5);

    // Verify the element was added
    Mockito.verify(spyList).add(5);

    // By default, contains() should return true for element 5
    assertTrue(spyList.contains(5));
    System.out.println("Stubbing Example - Default behavior: spyList.contains(5) = " + spyList.contains(5));

    // Stub the contains() method to always return false for the value 5
    Mockito.doReturn(false).when(spyList).contains(5);

    // Now contains() should return false, even though the element is in the list
    assertFalse(spyList.contains(5));
    System.out.println("Stubbing Example - After stubbing: spyList.contains(5) = " + spyList.contains(5));

    // The element is still in the list (stubbing didn't change the list content)
    assertEquals(1, spyList.size());
    assertEquals(Integer.valueOf(5), spyList.get(0));
    System.out.println("Stubbing Example - Spy List Content: " + spyList);
}

スタブ化に関する重要な注意点

スパイメソッドをスタブ化する際は、when() の代わりに doReturn()doThrow()doAnswer() などを推奨します。これは、スパイの場合、when().thenReturn() 構文がスタブ化中に実際のメソッドを呼び出す可能性があり、予期しない副作用を引き起こす可能性があるためです。

例:

// This might call the real get() method, causing issues if index 10 doesn't exist
Mockito.when(spyList.get(10)).thenReturn(99); // May throw IndexOutOfBoundsException

// This is the correct way to stub a spy
Mockito.doReturn(99).when(spyList).get(10); // Safe, doesn't call the real method

NotAMockException の処理

通常のオブジェクト(モックまたはスパイではない)で Mockito の検証メソッドを使用しようとすると、NotAMockException が発生します。この例外を示すテストメソッドを追加しましょう。

@Test
public void testNotAMockException() {
    try {
        // Create a regular ArrayList (not a mock or spy)
        ArrayList<Integer> regularList = new ArrayList<>();
        regularList.add(5);

        // Try to verify an interaction on a regular object
        Mockito.verify(regularList).add(5);

        fail("Expected NotAMockException was not thrown");
    } catch (NotAMockException e) {
        // Expected exception
        System.out.println("NotAMockException Example - Caught expected exception: " + e.getMessage());
        assertTrue(e.getMessage().contains("Argument passed to verify() is not a mock"));
    }
}

更新されたコードをコンパイルしましょう。

cd ~/project
javac -cp lib/junit.jar:lib/mockito-core.jar:lib/byte-buddy.jar:lib/byte-buddy-agent.jar:lib/objenesis.jar:lib/hamcrest-core.jar MockitoSpyDemo.java

コードはエラーなしでコンパイルされるはずです。このステップでは、スパイメソッドをスタブしてデフォルトの動作をオーバーライドする方法と、通常のオブジェクトで検証メソッドを使用する際に発生する NotAMockException を処理する方法を学びました。

次のステップでは、モックとスパイを比較して、その主な違いを理解します。

Mock と Spy の違いを理解する (Mockito)

このステップでは、モックとスパイを直接比較して、その主な違いを理解します。この比較は、テストのニーズに適したツールを選択するのに役立ちます。

比較テストの作成

モックとスパイの違いを示すテストメソッドを追加しましょう。

@Test
public void testMockVsSpyDifference() {
    // Create a mock and a spy of ArrayList
    ArrayList<Integer> mockList = Mockito.mock(ArrayList.class);
    ArrayList<Integer> spyList = Mockito.spy(new ArrayList<>());

    // Add elements to both
    mockList.add(1);
    spyList.add(1);

    // Verify interactions (both will pass)
    Mockito.verify(mockList).add(1);
    Mockito.verify(spyList).add(1);

    // Check the size of both lists
    System.out.println("Mock vs Spy - Mock List Size: " + mockList.size());
    System.out.println("Mock vs Spy - Spy List Size: " + spyList.size());

    // Mock returns default values (0 for size) unless stubbed
    assertEquals(0, mockList.size());

    // Spy uses real method implementation, so size is 1
    assertEquals(1, spyList.size());

    // Stub both to return size 100
    Mockito.when(mockList.size()).thenReturn(100);
    Mockito.when(spyList.size()).thenReturn(100);

    // Both should now return 100 for size
    assertEquals(100, mockList.size());
    assertEquals(100, spyList.size());

    System.out.println("Mock vs Spy (After Stubbing) - Mock List Size: " + mockList.size());
    System.out.println("Mock vs Spy (After Stubbing) - Spy List Size: " + spyList.size());
}

主な違いの理解

Mockito におけるモックとスパイの主な違いをまとめましょう。

  1. メソッドの動作:

    • Mock: すべてのメソッドは、スタブ化されていない限り、デフォルト値(null、0、false)を返します。
    • Spy: メソッドは、スタブ化されていない限り、実際の実装を呼び出します。
  2. オブジェクトの状態:

    • Mock: 操作は状態を変更しません(例:モックリストへの追加は実際には何も追加しません)。
    • Spy: 操作は、実際のオブジェクトと同様に、オブジェクトの状態を変更します。
  3. 作成:

    • Mock: クラスから作成され、インスタンスからではありません(Mockito.mock(ArrayList.class))。
    • Spy: 通常、インスタンスから作成されます(Mockito.spy(new ArrayList<>()))。
  4. ユースケース:

    • Mock: 完全に動作を制御し、実際のメソッドを必要としない場合。
    • Spy: 実際のメソッドを使用したいが、一部の動作のみをオーバーライドする必要がある場合。

実用的な例:スパイを使用する場合

スパイの一般的なユースケースである、複雑なオブジェクトの部分的なスタブ化を示すテストをもう一つ追加しましょう。

@Test
public void testWhenToUseSpy() {
    // Create a complex data object that we want to partially mock
    StringBuilder builder = new StringBuilder();
    StringBuilder spyBuilder = Mockito.spy(builder);

    // Use the real append method
    spyBuilder.append("Hello");
    spyBuilder.append(" World");

    // Verify the real methods were called
    Mockito.verify(spyBuilder).append("Hello");
    Mockito.verify(spyBuilder).append(" World");

    // The real methods modified the state
    assertEquals("Hello World", spyBuilder.toString());
    System.out.println("When to Use Spy - Content: " + spyBuilder.toString());

    // Stub the toString() method to return something else
    Mockito.when(spyBuilder.toString()).thenReturn("Stubbed String");

    // Now toString() returns our stubbed value
    assertEquals("Stubbed String", spyBuilder.toString());
    System.out.println("When to Use Spy - After Stubbing toString(): " + spyBuilder.toString());

    // But other methods still work normally
    spyBuilder.append("!");
    Mockito.verify(spyBuilder).append("!");

    // toString() still returns our stubbed value
    assertEquals("Stubbed String", spyBuilder.toString());
}

更新されたコードをコンパイルしましょう。

cd ~/project
javac -cp lib/junit.jar:lib/mockito-core.jar:lib/byte-buddy.jar:lib/byte-buddy-agent.jar:lib/objenesis.jar:lib/hamcrest-core.jar MockitoSpyDemo.java

コードはエラーなしでコンパイルされるはずです。このステップでは、モックとスパイの主な違いと、それぞれを使用するタイミングを学びました。モックは元のオブジェクトの動作を完全に置き換えますが、スパイは元の動作を維持しつつ、必要に応じて特定のメソッドをオーバーライドできます。

まとめ

この実験では、Java の単体テストのための強力な機能である Mockito のスパイの概念を探求しました。以下は、私たちがカバーした主なポイントです。

  1. スパイの作成: Mockito.spy() メソッドと @Spy アノテーションの両方を使用してスパイを作成する方法を学びました。前者はより柔軟で、後者はスパイが多数ある場合に便利です。

  2. スパイの動作: スパイはモックのようにメソッド呼び出しを追跡しますが、デフォルトでは実際のメソッドを実行します。この二重の性質により、インタラクションを検証する機能を維持しながら、実際のオブジェクトをテストするのに特に役立ちます。

  3. スパイのスタブ化: doReturn()doThrow()、およびその他のスタブ化メソッドを使用して、スパイメソッドのデフォルトの動作をオーバーライドする方法を発見しました。これにより、他の動作をそのまま維持しながら、特定の動作を制御できます。

  4. 例外処理: 通常のオブジェクトで Mockito の検証メソッドを使用しようとしたときに発生する NotAMockException を調べ、それを適切に処理する方法を検討しました。

  5. Mock と Spy: モックとスパイを比較して、その主な違いを理解しました。モックは、元の動作をデフォルトまたはスタブされた応答で完全に置き換えますが、スパイは元の動作を維持しつつ、特定のメソッドをオーバーライドできます。

スパイは、Mockito の強力なツールであり、モックの完全な制御と実際のオブジェクトのリアリズムのバランスを提供します。これは、特に次のような場合に役立ちます。

  • 実際のオブジェクトをテストしたいが、特定のインタラクションを検証する必要がある場合
  • オブジェクトの残りの動作をそのまま維持しながら、特定のメソッドのみをオーバーライドする必要がある場合
  • すべてのメソッドをモックすることが非現実的な複雑なオブジェクトを扱っている場合

スパイをいつ、どのように使用するかを理解することで、Java アプリケーションのより効果的で保守性の高い単体テストを作成できます。