Mockitoを使用する場合のモッキングとスパイの違いは何ですか?


137

Mockitoスパイの使用のユースケースは何ですか?

callRealMethodを使用すると、すべてのスパイのユースケースをモックで処理できるように思えます。

私が見ることができる1つの違いは、ほとんどのメソッド呼び出しを実際のものにしたい場合、モック対スパイを使用するためにコードのいくつかの行を節約することです。それですか、それとも全体像が欠けていますか?

回答:


100

答えはドキュメントあります

実際の部分モック(1.8.0以降)

最後に、メーリングリストでの内部での多くの議論と議論の後、部分的なモックサポートがMockitoに追加されました。以前は、コードの匂いとして部分的なモックを考慮していました。ただし、部分的なモックの正当な使用例が見つかりました。

1.8より前のリリースでは、spy()は実際の部分モックを生成せず、一部のユーザーを混乱させていました。スパイの詳細については、こちらまたはjavadocのspy(Object)メソッドをご覧ください。

callRealMethod()はの後に導入されましたがspy()、後方互換性を確保するために、spy()はもちろん残されました。

そうでなければ、あなたは正しいです:スパイのすべての方法は、スタブされない限り本物です。モックのすべてのメソッドは、callRealMethod()呼び出されない限りスタブされます。一般的に、私はの使用を好みcallRealMethod()ます。これdoXxx().when()は、従来の代わりにイディオムを使用することを強制しないためです。when().thenXxx()


これらのケースでスパイよりもモックを優先する場合の問題は、クラスに挿入されていない(ただしローカルで初期化された)メンバーがクラスで使用され、後で「実際の」メソッドによって使用される場合です。モックでは、メンバーはデフォルトのJava値に初期化されます。これにより、誤った動作やNullPointerExceptionが発生する可能性があります。これを渡す方法は、「init」メソッドを追加してから「実際に」それを呼び出すことですが、それは私には少しやり過ぎのようです。
Eyal Roth、2014

ドキュメントから:「スパイは、たとえば、レガシーコードを扱う場合などに、慎重かつ時折使用する必要があります。」単体テストのスペースは、同じことを行う方法が多すぎることに悩まされています。
gdbj 2017年

89

スパイとモックの違い

Mockitoがモックを作成するとき–実際のインスタンスからではなく、タイプのクラスから作成します。モックは、クラスとの相互作用を追跡するために完全に装備された、クラスの必要最小限のシェルインスタンスを作成するだけです。一方、スパイは既存のインスタンスをラップします。それでも、通常のインスタンスと同じように動作します。唯一の違いは、それとのすべての相互作用を追跡するように装備されることです。

次の例では、ArrayListクラスのモックを作成します。

@Test
public void whenCreateMock_thenCreated() {
    List mockedList = Mockito.mock(ArrayList.class);

    mockedList.add("one");
    Mockito.verify(mockedList).add("one");

    assertEquals(0, mockedList.size());
}

ご覧のとおり、モックリストに要素を追加しても実際には何も追加されません。他の副作用なしでメソッドを呼び出すだけです。一方、スパイは異なる動作をします–実際にaddメソッドの実際の実装を呼び出し、要素を基になるリストに追加します。

@Test
public void whenCreateSpy_thenCreate() {
    List spyList = Mockito.spy(new ArrayList());
    spyList.add("one");
    Mockito.verify(spyList).add("one");

    assertEquals(1, spyList.size());
}

ここで、オブジェクトの実際の内部メソッドが呼び出されたと言えます。size()メソッドを呼び出すと、サイズは1になりますが、このsize()メソッドはモックされていません。では、1はどこから来るのでしょうか。size()がモック(またはスタブ)されていないため、内部の実際のsize()メソッドが呼び出されるため、エントリが実際のオブジェクトに追加されたと言えます。

出典:http : //www.baeldung.com/mockito-spy + self notes。


1
size()が1を返すという意味ですか?

最初の例で、そのメソッドもスタブされていないのになぜmockedList.size()戻るの0ですか?メソッドの戻り値の型が与えられた場合、それは単なるデフォルト値ですか?
マイク2016

@mike:をmockedList.size()返し、Javaのintデフォルト値intは0です。のassertEquals(0, mockedList.size());mockedList.clear();に実行しようとすると、結果は同じままです。
realPK 2017年

2
この答えはよく簡単に書かれていて、モックとスパイの違いをようやく理解するのに役立ちました。良いですね。
PesaThe

38

8つのメソッドを持つオブジェクトがあり、7つの実際のメソッドを呼び出して1つのメソッドをスタブするテストがある場合、2つのオプションがあります。

  1. モックを使用する場合は、7つのcallRealMethodを呼び出して1つのメソッドをスタブすることにより、モックを設定する必要があります。
  2. を使用して、spy1つのメソッドをスタブして設定する必要があります

公式ドキュメントdoCallRealMethodは、部分的なモックにスパイを使用することを推奨しています。

部分モックの詳細については、javadoc spy(Object)も参照してください。Mockito.spy()は、部分的なモックを作成するための推奨される方法です。その理由は、spy()メソッドに渡されたオブジェクトを作成する責任があるため、正しく作成されたオブジェクトに対して実際のメソッドが呼び出されることが保証されるためです。


5

スパイは、レガシーコードの単体テストを作成する場合に役立ちます

ここで実行可能な例をhttps://www.surasint.com/mockito-with-spy/で作成しました。その一部をここにコピーします。

このコードのようなものがあれば:

public void transfer(  DepositMoneyService depositMoneyService, WithdrawMoneyService withdrawMoneyService, 
             double amount, String fromAccount, String toAccount){
    withdrawMoneyService.withdraw(fromAccount,amount);
    depositMoneyService.deposit(toAccount,amount);
}

DepositMoneyServiceとWithdrawMoneyServiceをモックするだけなので、スパイは必要ないかもしれません。

しかし、いくつかのレガシーコードでは、依存関係は次のようなコードにあります。

    public void transfer(String fromAccount, String toAccount, double amount){

        this.depositeMoneyService = new DepositMoneyService();
        this.withdrawMoneyService = new WithdrawMoneyService();

        withdrawMoneyService.withdraw(fromAccount,amount);
        depositeMoneyService.deposit(toAccount,amount);
    }

はい、最初のコードに変更できますが、その後APIが変更されます。この方法が多くの場所で使用されている場合は、それらすべてを変更する必要があります。

代わりに、次のように依存関係を抽出できます。

    public void transfer(String fromAccount, String toAccount, double amount){
        this.depositeMoneyService = proxyDepositMoneyServiceCreator();
        this.withdrawMoneyService = proxyWithdrawMoneyServiceCreator();

        withdrawMoneyService.withdraw(fromAccount,amount);
        depositeMoneyService.deposit(toAccount,amount);
    }
    DepositMoneyService proxyDepositMoneyServiceCreator() {
        return new DepositMoneyService();
    }

    WithdrawMoneyService proxyWithdrawMoneyServiceCreator() {
        return new WithdrawMoneyService();
    }

次に、次のように依存関係を挿入するスパイを使用できます。

DepositMoneyService mockDepositMoneyService = mock(DepositMoneyService.class);
        WithdrawMoneyService mockWithdrawMoneyService = mock(WithdrawMoneyService.class);

    TransferMoneyService target = spy(new TransferMoneyService());

    doReturn(mockDepositMoneyService)
            .when(target).proxyDepositMoneyServiceCreator();

    doReturn(mockWithdrawMoneyService)
            .when(target).proxyWithdrawMoneyServiceCreator();

上記のリンクの詳細。


0

Mockベアダブルオブジェクトです。このオブジェクトには同じメソッドシグネチャがありますが、実現は空で、デフォルト値-0およびnullを返します

Spy複製されたdoubleオブジェクトです。新しいオブジェクトは実際のオブジェクトに基づいて複製されますが、それを模擬する可能性があります

class A {

    String foo1() {
        foo2();
        return "RealString_1";
    }

    String foo2() {
        return "RealString_2";
    }

    void foo3() {
        foo4();
    }

    void foo4() {

    }
}
@Test
public void testMockA() {

    //given
    A mockA = Mockito.mock(A.class);
    Mockito.when(mockA.foo1()).thenReturn("MockedString");

    //when
    String result1 = mockA.foo1();
    String result2 = mockA.foo2();

    //then
    assertEquals("MockedString", result1);
    assertEquals(null, result2);

    //Case 2
    //when
    mockA.foo3();

    //then
    verify(mockA).foo3();
    verify(mockA, never()).foo4();
}

@Test
public void testSpyA() {
    //given
    A spyA = Mockito.spy(new A());

    Mockito.when(spyA.foo1()).thenReturn("MockedString");

    //when
    String result1 = spyA.foo1();
    String result2 = spyA.foo2();

    //then
    assertEquals("MockedString", result1);
    assertEquals("RealString_2", result2);

    //Case 2
    //when
    spyA.foo3();

    //then
    verify(spyA).foo3();
    verify(spyA).foo4();
}

【ダブルタイプテスト】

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.