日々常々

ふつうのプログラマがあたりまえにしたいこと。

JUnit4.11でassertThatの失敗メッセージが良くなると聞いて

なんか変わるみたい。

挨拶文パクリ

こんばんは!
MavenセントラルにJUnit 4.11-beta-1が来てたことにようやく気が付きました!
で、早速 build.gradle に

dependencies {
  testCompile 'junit:junit:4.11-beta-1'
}

などと書いて体験します。

変わったとこ確認

JUnit4.10 までの assertThat さんはこうでした。

	public static <T> void assertThat(String reason, T actual,
			Matcher<T> matcher) {
		if (!matcher.matches(actual)) {
			Description description= new StringDescription();
			description.appendText(reason);
			description.appendText("\nExpected: ");
			description.appendDescriptionOf(matcher);
			description.appendText("\n     got: ");
			description.appendValue(actual);
			description.appendText("\n");
			throw new java.lang.AssertionError(description.toString());
		}
	}

appendValue はだいたい toString 結果を表示しちゃいますので、gotには actual の文字列表現で固定だったんです。これを誤魔化すために Matcher の describeTo をいじって expected に actual を混ぜ込んだりする涙ぐましい努力をしたりするわけです。でもやっぱ誤魔化しなんですよね、なんか、うん。

JUnit4.11-beta-1 の assertThat さんはこうなってます。

    public static <T> void assertThat(String reason, T actual,
            Matcher<? super T> matcher) {
        MatcherAssert.assertThat(reason, actual, matcher);
    }

なんて潔い……。

そしてこの MatcherAssert#assertThat さんは、hamcrest-coreの1.2(4年前)にございました*1 *2。実装はこの通り。

    public static <T> void assertThat(String reason, T actual, Matcher<? super T> matcher) {
        if (!matcher.matches(actual)) {
            Description description = new StringDescription();
            description.appendText(reason)
                       .appendText("\nExpected: ")
                       .appendDescriptionOf(matcher)
                       .appendText("\n     but: ");
            matcher.describeMismatch(actual, description);
            
            throw new AssertionError(description.toString());
        }
    }

describeMismatch の実装は Matcher が actual と description を受けていることからもわかる通り、受け取った description に actual を Matcher の都合のいいようにアレして追加する形です。

車輪を再発明しよう

とりあえずこんな感じで書く。

@Test
public void testName() throws Exception {
	List<?> actual = Arrays.asList("H", "O", "G", "E");
	assertThat(actual, size(3));
}

size はもちろんオレオレMatcherだ。

private Matcher<Collection<?>> size(final int size) {
	return new TypeSafeMatcher<Collection<?>>() {
		@Override
		public void describeTo(Description description) {
			description.appendText("number of elements is " + size);
		}

		@Override
		public boolean matchesSafely(Collection<?> actual) {
			return actual.size() == size;
		}
	};
}

これで実行する。テストはこける。結果はもちろん残念なアレだ。

java.lang.AssertionError: 
Expected: number of elements is 3
     got: <[H, O, G, E]>

JUnit4.11-beta-1 ではこんな感じで書けばいいらしい。

//...
		public void describeMismatch(Collection<?> actual, Description description) {
			description.appendText("number of elements was " + actual.size());
		}
//...

書いた所でもちろん何も変わらない。


ならば自分に都合のいい assertThat を作ればいい。

public static <T> void assertThat(String reason, T actual, Matcher<T> matcher) {
	if (!matcher.matches(actual)) {
		Description description = new StringDescription();
		description.appendText(reason);
		description.appendText("\nExpected: ");
		description.appendDescriptionOf(matcher);
		description.appendText("\n     got: ");
		appendActualDescription(actual, matcher, description);
		description.appendText("\n");
		throw new java.lang.AssertionError(description.toString());
	}
}

private static <T> void appendActualDescription(T actual, Matcher<T> matcher, Description description)
		throws AssertionError {
	try {
		// describeMismatchメソッドがあったらそれ
		Method describeMismatch = getDescribeMismatchMethod(matcher);
		if (describeMismatch != null) {
			describeMismatch.invoke(matcher, actual, description);
			return;
		}
		// 何も無かったら今まで通り
		description.appendValue(actual);
	} catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException e) {
		throw new AssertionError(e);
	}
}

private static <T> Method getDescribeMismatchMethod(Matcher<T> matcher) throws IllegalArgumentException,
		IllegalAccessException {
	for (Method m : matcher.getClass().getMethods()) {
		if (m.isAnnotationPresent(DescribeMismatch.class)) {
			return m;
		}
	}
	return null;
}

これで。

java.lang.AssertionError: 
Expected: number of elements is 3
     got: number of elements was 4

いけた。ひゃっほう。

なんで車輪を再発明したか

インターネットに接続できないからです。最新のライブラリをいきなり使うのは難しかったりするからです。それでも自分で書いたものであれば、なんとなくぼんやり使えたりします。独自技術症候群万歳です。それもどうなのですが、挙動を把握してるならいい気もします。把握してるなら最新のものを使えって話にもなる気もするあたり、穴だらけの理論ですが。
再発明することで挙動を理解できたりもします。「車輪の再発明の効用」です。ちょっとしたものであれば、欲しい機能だけピンポイントで取り入れたりも出来ます。

ここで書いているのはJUnit4.4+Hamcrest1.1とかでもいけたりします。なんでそんなバージョンかと言うと、現場で使えるJUnitのバージョンがそんな感じだったりするから。JUnitをダウンロードできなくてもEclipseに入ってますので使えたりします。さすがにEclipseも使えないってのはあまり無いですし……。多少バージョン古くても、こんな感じであれこれ色々注入してけば、それなりに快適なJUnitライフが送れたりします。この程度の再発明は10分やそこらですしね。

おまけ

今回書いたのはGitHubに上げてたり。上記のだとisとか使っちゃうと途切れちゃうので、中身引っ張りだすようにしたり。やっつけ仕事感パない。完全な車輪が欲しければ、素直にバージョンアップですよ。

*1:つまりJUnit待たなくてもhamcrest上げれば使えたんですね……

*2:たまにJUnit4.10以前でhamcrestあげてるとこっち呼んじゃってそんなメソッドねーよエラーがでたりしたりしたり。