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

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

続・Matchers#anyの謎

先日の日記で、『Matchers#any系はインラインで使用する必要がある』と書きました。


で、「インラインで呼び出しているか否かをどうやって検知しているのか、そのうち調べます」と予告していた通り、
今日はその件について調査しました。以下、結論にたどり着くまでの過程を記します。

サンプルコード

このサンプルコードを基に調査を開始。

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

import java.util.Arrays;
import java.util.Collection;
import java.util.Set;

public class Main<T> {

	public static void main(String... args) throws Exception {

		int i = 0;

		final Foo foo = spy(new Foo());
		final Set<?> anySet = anySet();

		/* STEP */System.err.println("STEP:" + ++i);

		assert (anySet == null);

		/* STEP */System.err.println("STEP:" + ++i);

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

		/* STEP */System.err.println("STEP:" + ++i);

		foo.doSomething();

		/* STEP */System.err.println("STEP:" + ++i);

		verify(foo).doOtherthing(anySet());

		/* STEP */System.err.println("STEP:" + ++i);
	}
}

class Foo {

	void doSomething() {
		this.doOtherthing(Arrays.asList(1, 2, 3));
	}

	void doOtherthing(Collection<?> c) {
		System.out.println(c);
	}
}

まず手始めはエラーメッセージから

このコードを実行すると、このようなエラーメッセージが表示されます。

STEP:1
STEP:2
Exception in thread "main" org.mockito.exceptions.misusing.InvalidUseOfMatchersException: 
Misplaced argument matcher detected here:
-> at Main.main(Main.java:15)

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 Main.main(Main.java:23)


まずはこの箇所に注目。

Misplaced argument matcher detected here:
-> at Main.main(Main.java:15)

で、コードの15行目とは、

final Set<?> anySet = anySet();

です。どうやらMatchers#anyを実行した時点で何らかの情報を記録している模様。


で、次に注目すべきなのは

Following methods *cannot* be stubbed/verified: final/private/equals()/hashCode().

	at Main.main(Main.java:23)

で、コードの23行目とは

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

です。23行目の時点で、Mockitoのルールに違反していることを検知(InvalidUseOfMatchersException)しているわけです。

次に調べるべきことは、

InvalidUseOfMatchersExceptionをスローしている箇所の特定。


Mockitoのソースをダウンロード&展開の上、以下のコマンドで探しました。

find ${MOCKITO_SRC} -name '*.java' | xargs grep -n 'outside of verification or stubbing'


この実行結果は次の通り。

./mockito/exceptions/Reporter.java:420:	"You cannot use argument matchers outside of verification or stubbing.",


はい、発見。

Reporter.javaの420行目付近は

こんな感じ。

public void misplacedArgumentMatcher(Location location) {
    throw new InvalidUseOfMatchersException(join(
            "Misplaced argument matcher detected here:",
            location,
            "",
            "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().",
            ""
            ));
}

余談ですが、Javaにはヒアドキュメントがありません。
だからこんな不恰好なコードになっちゃう。少し不便ですねぇ。。。


さて、Reporter#misplacedArgumentMatcherを呼び出しているのは、

org.mockito.internal.progress.ArgumentMatcherStorageImpl

の103行目です。


この付近のコードは

    /* (non-Javadoc)
     * @see org.mockito.internal.progress.ArgumentMatcherStorage#validateState()
     */
    public void validateState() {
        if (!matcherStack.isEmpty()) {
            LocalizedMatcher lastMatcher = matcherStack.lastElement();
            matcherStack.clear();
            new Reporter().misplacedArgumentMatcher(lastMatcher.getLocation());
        }
    }

です。で、このvalidateStateを呼び出している箇所は

org.mockito.internal.progress.MockingProgressImpl#validateState

の83行目です。

さあ困った

どうしよう。


と言うのも、MockingProgressImpl#validateStateを呼び出している箇所が複数あるからです。
なんかこれ以上追っかけるのがメンドクサイ。


というわけで荒業を使いました。ソースの書き換えです。

Reporter#misplacedArgumentMatcherをこんな感じに書き換えました。

public void misplacedArgumentMatcher(Location location) {
    new Throwable().printStackTrace(); // ←ココ!!

    throw new InvalidUseOfMatchersException(join(
            "Misplaced argument matcher detected here:",
            location,
            "",
            "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().",
            ""
            ));
}

で、実行した結果、こんなスタックトレースを採集できました。

(スタックトレース1)

java.lang.Throwable
	at org.mockito.exceptions.Reporter.misplacedArgumentMatcher(Reporter.java:288)
	at org.mockito.internal.progress.ArgumentMatcherStorageImpl.validateState(ArgumentMatcherStorageImpl.java:103)
	at org.mockito.internal.progress.MockingProgressImpl.validateState(MockingProgressImpl.java:83)
	at org.mockito.internal.progress.MockingProgressImpl.stubbingStarted(MockingProgressImpl.java:62)
	at org.mockito.internal.progress.ThreadSafeMockingProgress.stubbingStarted(ThreadSafeMockingProgress.java:44)
	at org.mockito.internal.MockitoCore.doAnswer(MockitoCore.java:134)
	at org.mockito.Mockito.doNothing(Mockito.java:1415)
	at Main.main(Main.java:23)


これを、以下のような『正常なテストコード』の実行結果として採集したスタックトレースと比較してみます。

public class Main<T> {

	public static void main(String... args) throws Exception {
		final Foo foo = spy(new Foo());
		doNothing().when(foo).doOtherthing(anySet());
	}
}

で、スタックトレースはコチラ。
(スタックトレース2)

java.lang.Throwable
	at org.mockito.exceptions.Reporter.misplacedArgumentMatcher(Reporter.java:288)
	at org.mockito.internal.progress.ArgumentMatcherStorageImpl.validateState(ArgumentMatcherStorageImpl.java:103)
	at org.mockito.internal.progress.MockingProgressImpl.validateState(MockingProgressImpl.java:83)
	at org.mockito.internal.progress.MockingProgressImpl.stubbingStarted(MockingProgressImpl.java:62)
	at org.mockito.internal.progress.ThreadSafeMockingProgress.stubbingStarted(ThreadSafeMockingProgress.java:44)
	at org.mockito.internal.MockitoCore.doAnswer(MockitoCore.java:134)
	at org.mockito.Mockito.doNothing(Mockito.java:1415)
	at Main.main(Main.java:12)


。。。スタックトレースの1と2で、何も変わってないですね。ということは、
『Mockito自身がステートフルなオブジェクトとして振る舞い、メソッド呼び出しの前後関係を記録し検証している』
という線で確定でしょう、


ま、

org.mockito.internal.progress.MockingProgressImpl

なんてクラスがあるんだから、モロバレなんですがw

さて、結局のところは

『Matchers#any系がMockito#doAnswer系より先に呼ばれているか否か』で判定しているということで確定です。


どういう事かというと、例えば

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

を見てみると、

  1. Mockito#doNothing
  2. Mockito#when
  3. Matchers#anySet
  4. Foo#doSomething

という呼び出し順序です。で、コレは正常。


それに対し、

Set<?> set = anySet();
doNothing().when(foo).doOtherthing(anySet);

  1. Matchers#anySet
  2. Mockito#doNothing
  3. Mockito#when
  4. Foo#doSomething

という呼び出し順序です。で、Matchers#anyがMockito#doNothingより先に呼ばれているのでアウト!というわけです。


ま、結論だけ書いてみると

( ´_ゝ`)フーン

という感じなんですけどね。

ということは...

Stubber s = doNothing();

Foo mockingFoo = s.when(foo);

Set<?> anySet = anySet();

mockingFoo.doOtherthing(anySet);

なんて書いてやれば、Mockitoを騙せるということに。
そう。実はインライン呼び出しをする必要はなかった、というわけです。


ま、こうすることに何の意味があるかは置いておくとして、ですけど。