(注:このブログはもう更新していません)この日記は私的なものであり所属会社の見解とは無関係です。 GitHub: takahashikzn

[クラウド帳票エンジンDocurain]

メソッド引数をキャプチャする(ArgumentCaptorの使い方)

メソッドの途中で計算された値を、他のメソッドの引数として使うというケースをテストすることがままあります。


Mockitoを使ったテストは以下のようになるでしょう。

(テスト対象)

public class Foo {

    public void doSomething() {

        final StringBuilder sb = new StringBuilder();

        // sbに色々appendする処理

        this.doOtherthing(sb.toString());
    }

    void doOtherthing(String bar) {
        // 何かの処理
    }

}

(テスト)

import static org.mockito.Mockito.*;

public class FooTest {

    @Test
    public void doSomething() {

        final Foo foo = spy(new Foo());

        // Foo#doOtherthingの引数として渡されるはずの文字列
        final String expectedDoOtherthingArgument = "foobarbaz";

        // doOtherthingの中身を空実装にしておく
        // (ここはdoSomethingのテストなので、他の要素は排除すべき)
        doNothing().when(foo).doOtherthing(expectedDoOtherthingArgument);

        foo.doSomething();

        // doOtherthingが期待通りに実行されているか検証
        verify(foo).doOtherthing(expectedDoOtherthingArgument);
    }

}

ま、Mockito#spyが大活躍!といった感じです。

ところが、

こんなクラスをテストする場合、上記の方法が使えません。

public class Bar {

    private String prop1;
    private int prop2;
    private List<String> prop3;

    // 以下、getter,setterのみ実装。
    // equalsとhashCodeは実装されていない。
}


public class Foo {

    public void doSomething() {

        final Bar bar = new Bar();

        bar.setProp1("1");
        bar.setProp2(2);
        bar.setProp3(Arrays.asList("3.1", "3.2"));

        this.doOtherthing(bar);
    }

    void doOtherthing(Bar bar) {
        // 何かの処理
    }

}

(テスト)

import static org.mockito.Mockito.*;

public class FooTest {

    @Test
    public void doSomething() {

        final Foo foo = spy(new Foo());

        // Foo#doOtherthingの引数として渡されるはずのBar
        final Bar expectedBar = new Bar();
        expectedBar.setProp1("1");
        expectedBar.setProp2(2);
        expectedBar.setProp3(Arrays.asList("3.1", "3.2"));

        doNothing().when(foo).doOtherthing(expectedBar);

        foo.doSomething();

        // doOtherthingが期待通りに実行されているか検証…?
        verify(foo).doOtherthing(expectedBar);
    }
}


このテストは失敗します。何故かというと、FooTestの

doNothing().when(foo).doOtherthing(expectedBar);

で渡しているexpectedBarにはequalsメソッドが実装されていないので、
たとえ事実上『等価』であるBarを渡しても、実際にFoo#doSomethingの中で作成されたBarと
等しいとはみなされないからです。


当然ながら、これもダメです。


(テスト)

import static org.mockito.Mockito.*;
import static org.mockito.Matchers.*;

public class FooTest {

    @Test
    public void doSomething() {

        final Foo foo = spy(new Foo());

        // Foo#doOtherthingの引数として渡されるはずのBar
        final Bar expectedBar = new Bar();
        expectedBar.setProp1("1");
        expectedBar.setProp2(2);
        expectedBar.setProp3(Arrays.asList("3.1", "3.2"));

        // どんなBarであろうとも、とにかくdoOtherthingの中身を空実装にする
        doNothing().when(foo).doOtherthing(any(Bar.class));

        foo.doSomething();

        // doOtherthingが期待通りに実行されているか検証…?
        verify(foo).doOtherthing(expectedBar);
    }
}


これだとdoNothingのところはうまく動きますが、結局のところ

verify(foo).doOtherthing(expectedBar);

での検証に失敗します。もちろん

verify(foo).doOtherthing(any(Bar.class));

とすれば動きますが、これでは単体テストの意味が無い。



この問題は、Barにequalsメソッドを実装すれば解決するわけですが、
それが常に出来るとは限らないわけです。(他社のコンポーネントだから手を入れられないとか)

ArgumentCaptorさんコンニチハ


そこでどうするかと、ArgumentCaptorさんの登場となるわけです。
こんな風に使います。

import static org.junit.Assert.*;
import static org.mockito.Mockito.*;

public class FooTest {

    @Test
    public void doSomething() {

        final Foo foo = spy(new Foo());

        // 引数キャプター
        final ArgumentCaptor<Bar> barCaptor = ArgumentCaptor.forClass(Bar.class);

        // doOtherthingの中身を空実装にする。ついでに引数を捕獲する
        doNothing().when(foo).doOtherthing(barCaptor.capture());

        foo.doSomething();

        // 捕獲した引数
        final Bar captoredBar = barCaptor.getValue();

        assertEquals("1", captoredBar.getProp1());
        assertEquals(2, captoredBar.getProp2());
        assertEquals(Arrays.asList("3.1", "3.2"), captoredBar.getProp3());
    }
}


このように、引数を捕獲して後からassertできるというわけ。

ArgumentCaptorさんの弱点。それは…


特定のシチュエーションで確かに便利なArgumentCaptorさんですが、一点だけ注意が。

public class Foo {

    public void doSomething() {
        doOtherthing(..., "foobar");
    }

    void doOtherthing(Bar bar, String s) {
    }
}

public class Bar { ... }

public class FooTest {

    @Test
    public void doSomething() {

        final Foo foo = spy(new Foo());

        // Foo#doOtherthingの引数として渡されるはずのBar
        final ArgumentCaptor<Bar> bar1Captor = ArgumentCaptor.forClass(Bar.class);

        // doOtherthingの中身を空実装にする。ついでに第一引数を捕獲する。
        // 第二引数は"foobar"だと分かっているので直接指定…?
        doNothing().when(foo).doOtherthing(barCaptor.capture(), "foobar");

        foo.doSomething();

        // 以下略
    }
}


こんなふうにテストコードを書いて実行すると、

doNothing().when(foo).doOtherthing(barCaptor.capture(), "foobar");


の箇所でMockitoのInvalidUseOfMatchersExceptionがスローされてしまいます。
この例外は名前が示す通り、Mockitoの使い方を間違えている場合にスローされるものです。

org.mockito.exceptions.misusing.InvalidUseOfMatchersException: 
Invalid use of argument matchers!
3 matchers expected, 2 recorded.
This exception may occur if matchers are combined with raw values:
    //incorrect:
    someMethod(anyObject(), "raw String");
When using matchers, all arguments have to be provided by matchers.
For example:
    //correct:
    someMethod(anyObject(), eq("String by matcher"));

For more info see javadoc for Matchers class.
	at XXX.doSomething(FooTest.java:??)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
	at java.lang.reflect.Method.invoke(Method.java:597)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:76)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:193)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:52)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:191)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:42)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:184)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:236)
	at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:46)
	at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)


何故かというと、ArgumentCaptorは一種のMatcherとして振舞うから。
こちらの記事で解説している通りですが、『Matcherを使うなら全ての引数で使わないとダメ』
というルールに違反しているからです。(※ただしMatchers#eqを使えば、事実上Matcherの部分適用ができる事に注意)


というわけで、こんな風に修正すれば意図した通りに動きます。

public class FooTest {

    @Test
    public void doSomething() {

        final Foo foo = spy(new Foo());

        // Foo#doOtherthingの引数として渡されるはずのBar
        final ArgumentCaptor<Bar> bar1Captor = ArgumentCaptor.forClass(Bar.class);

        // doOtherthingの中身を空実装にする。ついでに第一引数を捕獲する。
        //
        // 第二引数は"foobar"だと分かっているがanyStringにしないとダメ。
        // どうしても検証したいなら第二引数でも引数キャプターを使えばOK
        doNothing().when(foo).doOtherthing(barCaptor.capture(), anyString());

        foo.doSomething();

        // 以下略
    }
}