JUnitで例外メッセージのテストをするMatcherとRuleを書いたので

時代遅れ感漂う記事。

  • 正規表現でマッチングを行うMatcher
  • 例外メッセージのテストを行うためのRule
  • 例外メッセージのテスト専用のMatcher

を作ったので、備忘録程度に。
正しいかどうかは知らないけど、動いてるからOKとする。

正規表現でマッチングを行うMatcher

matchesはインタフェースで使われているので、メソッド名はmatchにします。

テストコードはこんなかんじです。色々詰め込んでいて悪い例ですが。

public RegexMatcherTest(){
	@Test
	public void regexMatcherTest() {
		// hogeが含まれているか
		assertThat("hogehoge", is(match("hoge")));

		// HOGEhogeに含まれているか(大文字小文字問わず)
		assertThat("hogehogefuga", is(match("HOGEhoge", Pattern.CASE_INSENSITIVE)));

		// HOGEが含まれているか、hogeで始まりhogeで終わるものにマッチ
		assertThat("hogehogefugahoge", anyOf(match("HOGE"), match("^(hoge)+.+\\1$")));

		// hogeで始まりhogeで終わるものにマッチ(正規表現の確認)
		assertThat("hogehogefugahoge", allOf(match("^hoge.+hoge$"), match("^(hoge).+\\1$")));
	}
}
RegexMatcher.java
public class RegexMatcher extends TypeSafeMatcher<String> {
	private final Pattern expectedPattern;

	public RegexMatcher(Pattern expectedPattern) {
		this.expectedPattern = expectedPattern;
	}

	/**
	 * 英語は間違えてると思います。
	 */
	@Override
	public void describeTo(org.hamcrest.Description description) {
		description.appendText("matched regexp \"" + expectedPattern.pattern() + "\"");
	}
	
	/**
	 * このメソッドが呼ばれるタイミングがわからない!!
	 */
	@Override
	public void describeMismatch(Object item, org.hamcrest.Description description) {
		super.describeMismatch(item, description);
	}

	@Override
	public boolean matchesSafely(String item) {
		return item != null && expectedPattern.matcher(item).find();
	}

	public static RegexMatcher match(String regex) {
		return expectedPatternIs(regex, 0x00);
	}

	public static RegexMatcher expectedPatternIs(String regex, int flags) {
		return new RegexMatcher(Pattern.compile(regex, flags));
	}
}


テストは全部通ってしまいますが、それではdescribeToの挙動がわからないので、
いくつかテストをわざと失敗するように変えてみます。

	@Test
	public void failedTest() {
		assertThat("hogehoge", is(match("hogeo")));
	}
java.lang.AssertionError: 
Expected: is matched regexp "hogeo"
     got: "hogehoge"

	at org.junit.Assert.assertThat(Assert.java:780)
	at org.junit.Assert.assertThat(Assert.java:738)
.....


期待した正規表現と値が一致していないことが確認できていい感じです。
allOfのほうの期待値を少し変えてみます。

	@Test
	public void failedTest() {
		assertThat("hogehogefugahoge", allOf(match("^fuga.+fuga$"), match("^(hoge).+\\1$")));
	}
java.lang.AssertionError: 
Expected: ( matched regexp "^fuga.+fuga$" and  matched regexp "^(hoge).+\1$")
     got: "hogehogefugahoge"

	at org.junit.Assert.assertThat(Assert.java:780)
	at org.junit.Assert.assertThat(Assert.java:738)
.....

ちょっと変な感じですが、期待している全ての正規表現のいずれにもマッチしていないというのがわかりますね。
うまく動いています。

例外メッセージのテストを行うためのRule

期待した例外を起きるか試すために@Test(expected=XXXX.class)ってのがありますが、
なんでもかんでもRuntimeExceptionで全部投げているライブラリ(うちのかいしゃにあります^0^)とかあると、RuntimeExceptionが起きることがわかっていても、
例外メッセージを見ないと、具体的なエラーがわからないことがしばしばあります。


なので、例外メッセージを見て、期待したメッセージかどうかチェックするようにします。
無駄に汎用性を上げるために正規表現でマッチングするようにします。


まず、Ruleを書きます。
しかし、Ruleは普通に描くとテストクラス全体に影響を与えてしまうので、
アノテーションがあったときだけ、そのRuleを適用するようにします。


テストコードはこんな感じに描けるようにします。
念のため、普通のテストと、正常終了しちゃった場合の確認もします。

public class ExpectedExceptionMessageTest{
	@Test
	public void okTest() {
		// ok
	}

	@Test
	@ExpectedExceptionMessage("hoge")
	public void hogeTest() {
	}

	@Test
	@ExpectedExceptionMessage("^(失敗した)+$")
	public void 失敗したTest() {
		fail("失敗した失敗した失敗した失敗した失敗した失敗した失敗した失敗した失敗した失敗した");
	}
}
ExpectedExceptionMessage.java
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD })
public @interface ExpectedExceptionMessage {
	String value();
}
ExpectedExceptionRule.java
public class ExpectedExceptionRule implements TestRule {
	@Override
	public Statement apply(final Statement base, final Description description) {
		final ExpectedExceptionMessage expectedExceptionMessage = description.getAnnotation(ExpectedExceptionMessage.class);
		if (expectedExceptionMessage == null) {
			return base;
		}

		return new Statement() {
			@Override
			public void evaluate() throws Throwable {
				try {
					base.evaluate();
					fail("No exception is thrown");
				} catch (Throwable e) {
					assertThat(e.getMessage(), is(RegexMatcher.match(expectedExceptionMessage.value())));
				}
			}
		};
	}
}
java.lang.AssertionError: 
Expected: is  matched regexp "hoge"
     got: "No exception is thrown"

	at org.junit.Assert.assertThat(Assert.java:780)
	at org.junit.Assert.assertThat(Assert.java:738)
.....

こんな感じにhogeTestだけ失敗します。
例外を投げず、正常に終了するテストケースなので、
期待した例外メッセージが取れなかったということで、例外を投げて失敗します。
期待通りといえば期待通りです。
で、これも例外メッセージなので、"No exception is thrown"を期待値にすれば、hogeTestもとおります。

	@Test
	@ExpectedExceptionMessage("No exception is thrown")
	public void hogeTest() {
	}

これでこのRuleが同じクラス内の他のテストと混ぜてもうまく動きそうな事は確認できました。


例外メッセージのテスト専用のMatcher

さて、さりげなく内部で先ほど作ったRegexMatcherを使ってますが、
これを一歩進めて、例外メッセージのテスト用のRegexMatcherを作ってみます。
内容は簡単なラッパークラスを作るだけで、
メッセージに一言、"exception message"を加えて、
テスト失敗時に例外メッセージにマッチしなかったことを明示的に出すようにするだけです。
そこまでやる必要があるのか、といえば無いのですが、
Matcherのラッパークラスが作れると気分がよくなるような感じがしたので。

ExpectedExceptionRegexMatcher.java
public class ExpectedExceptionRegexMatcher extends TypeSafeMatcher<String> {
	private final RegexMatcher matcher;

	public ExpectedExceptionRegexMatcher(RegexMatcher matcher) {
		this.matcher = matcher;
	}

	@Override
	public void describeTo(org.hamcrest.Description description) {
		description.appendText("exception message").appendDescriptionOf(matcher);
	}

	@Override
	public boolean matchesSafely(String item) {
		return matcher.matches(item);
	}

	public static ExpectedExceptionRegexMatcher expectedExceptionMessage(RegexMatcher regexMatcher) {
		return new ExpectedExceptionRegexMatcher(regexMatcher);
	}
}


ExpectedExceptionRuleをちょこっと修正します。
クラス名がダサかったら、static importを使ってください。

ExpectedExceptionRule.java
-					assertThat(e.getMessage(), is(RegexMatcher.match(expectedExceptionMessage.value())));
+ 					assertThat(e.getMessage(), ExpectedExceptionRegexMatcher.expectedExceptionMessage(RegexMatcher.match(expectedExceptionMessage.value())));
java.lang.AssertionError: 
Expected: exception message matched regexp "hoge"
     got: "No exception is thrown"

	at org.junit.Assert.assertThat(Assert.java:780)
	at org.junit.Assert.assertThat(Assert.java:738)
.....

実行すると、exception messageといった文言が付きます。
assertThat("No exception is thrown", match("hoge"));で失敗したときの出力と区別できます。

まとめ

static import...めんどい...どうにかして...*

はんせい

ExpectedExceptionMessageってあるのに正規表現で書くあたり、名前が間違ってる感。
クラス名とかメソッド名を付けるのに困らないぐらいの語呂はほしい...