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

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

Matchers#anyの謎

今日も懲りずにMockitoネタです。すいません。


近頃、興味深いエラーに出くわしました。

public class Foo {

    public void doSomething() {

        final Set<String> strings = ...;

        this.doOtherthins(strings);
    }

    void doOtherthing(final Set<String> strings) {
        // 何かの処理
    }
}

public class FooTest {

    @Test
    public void testDoSomething() {
        final Foo foo = spy(new Foo());

        doNothing().when(foo).doOtherthing(any(Set.class)); // ※

        foo.doSomething();

        verify(foo).doOtherthing(any(Set.class)); // ※
    }


このコードは動作するのですが、※の場所でコンパイラが警告を出します。
なぜなら、Mockito#anyは総称型の値を返せないからです。宣言を見てみると、

    /**
     * any kind object, not necessary of the given class.
     * The class argument is provided only to avoid casting.
     * <p>
     * Sometimes looks better than anyObject() - especially when explicit casting is required
     * <p>
     * Alias to {@link Matchers#anyObject()}
     * <p>
     * See examples in javadoc for {@link Matchers} class
     * 
     * @return <code>null</code>.
     */
    public static <T> T any(Class<T> clazz) {
        return (T) anyObject();
    }


というわけなので、anyの戻り値は素のSetになります。


だから、警告を消すために、次のようにしてみました。

public class Foo {

    public void doSomething() {

        final Set<String> strings = ...;

        this.doOtherthins(strings);
    }

    void doOtherthing(final Set<String> strings) {
        // 何かの処理
    }
}

public class FooTest {

    @Test
    public void testDoSomething() {
        final Foo foo = spy(new Foo());

        doNothing().when(foo).doOtherthing(any(Set.class));

        foo.doSomething();

        // 警告除去用
        @SuppressWarnings("unchecked")
        final Set<String> anySet = any(Set.class);

        verify(foo).doOtherthing(anySet);
    }

すると、

謎のエラーが発生。

org.mockito.exceptions.misusing.InvalidUseOfMatchersException: 
Misplaced argument matcher detected here:
-> at sample.FooTest.testDoSomething(FooTest.java:10)

You cannot use argument matchers outside of verification or stubbing.
Examples of correct usage of argument matchers:
    when(mock.get(anyInt())).thenReturn(null);
    doThrow(new RuntimeException()).when(mock).someVoidMethod(anyObject());
    verify(mock).someMethod(contains("foo"))

Also, this error might show up because you use argument matchers with methods that cannot be mocked.
Following methods *cannot* be stubbed/verified: final/private/equals()/hashCode().

    at sample.FooTest(FooTest.java:10)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    ...(略)


エラーメッセージによると、どうやらMockito#any系のメソッドはインラインで呼び出す必要があるようです。

つまり、

    doOtherthing(any(Set.class));

はOKだけど、

    final Set anySet = any(Set.class);
    doOtherthing(anySet);

はダメだということです。うーむ、何故??

どうやってるのコレ?

『インライン呼び出しかどうか』で制御を分けているなんてビックリです。

これまで、趣味・仕事問わず様々なソースを読んできましたが、どうやってコレを実現しているのか非常に気になるところです。
というわけで、Mockitoの内部で何が起きているのかを調べてみようと思います。


一番ベタな予想だとStackTraceを使って、メソッド呼び出し時の行番号を見ているのではないか?
と一瞬思ったのですが、それだとjavacのコンパイルオプションよって
実行結果が変化してしまう(デバッグ情報抜きでコンパイルした場合)のか。。。じゃ、それはないな。


ま、調査結果はそのうち。。。

ちなみに

上記の例は、フツーに

public <T> Mockito#anySetOf(Class<T> type)

を使えばOK。ご参考まで。