Java Advent Calendar 2011の16日目です。
書いてること
JUnit の話です。使い始めからちょっとだけ踏み込んだ辺りですかね。ちょっとだけなので普通に使ってる方には不要な内容かと思います。私の今持ってる知識を書き殴ってみた感じになりましたが、微妙な理解と残念な文章力の相乗効果でグダグダになってます。お察しください。
内容は Assertion->カスタムAssertion、Matcher->カスタムMatcher、Rule->カスタムRule です。
Assertion
JUnitは assert があってこそです。まず org.junit.Assert にある馴染み深い assert を並べてみます。
- assertEquals / assertNotEquals
- assertNull / assertNotNull
- assertArrayEquals
- assertSame / assertNotSame
- assertTrue / assertFalse
色々ありますけど、だいたい assertEquals と assertTrue が使えれば困ることはありません。他は使ったことが無いって方が居たとしても不思議じゃないかなと思ってます。
個別に見ていきますと、私はそもそも配列自体あまり使わないので assertArrayEquals は使いません。 assertSame が欲しくなる場面も正直ないです。assertNull は普通に使いますが、 assertNotNull は手を抜くとき*1に使うくらいかもしれません。なので太字にしているものでだいたいまかなえる感じです。
単に検証するだけなら assertTrue で事足りるのですが、引数が boolean になってしまうので失敗時の表現力に乏しいです。例えば assertEquals ならば「何と何を比較してどう違ったか」を出力することが出来ますので、 assertEquals を使える場面で assertTrue(aaa.equals(bbb)) なんてしてはいけません。検証失敗時にすぐにわかる内容を通知するためにも、適切な assertXxx を使用するべきです。そっちの方が単純に書くことが少なくて楽だと言うのもあります。
ベタに書いている時にはあまり使わないのですが、 assertXxx は失敗時に表示されるメッセージを指定出来ます。検証したい対象の日本語名とかを渡しておくと微妙に便利かもしれませんが、IDEを使っていれば、失敗した行に直接ジャンプ出来るのであまり嬉しくもありません。寧ろメッセージを書くコストが勿体ないです。地味に役立つ使い方としては、例えばループ内で assert する時とかにループカウンタを書いておくとかがあります。
@Test public void testFailMessage() throws Exception { int[] actual = { 1, 1, 1, 1, 2, 1 }; for (int i = 0; i < actual.length; i++) { assertEquals("count:" + i, 1, actual[i]); } } // java.lang.AssertionError: count:4 expected:<1> but was:<2>
この用途ならば assertArrayEquals を使用するのが正解ですが、例えば「件数は解らないけど全て同じ値が入っているべき」なんてことが…あったり……するかもしれないですよね!そう言うときに使えます!
真面目に書くと、同じ assert を使い回す時では、これが無いと使い物にならなかったりします。JUnitのテストは成功し続けるようにプロダクトコード共々メンテしていくものですが、失敗した時に意味が分かるようにしておくことは超重要です。意味が分からない失敗は害になります。
assert 絡みで気をつけなきゃなのは Deprecated になってる double の assertEquals ですかね。実装は必ず失敗するようになっています。
@Deprecated static public void assertEquals(String message, double expected, double actual) { fail("Use assertEquals(expected, actual, delta) to compare floating-point numbers"); }
そもそも double 使うことも無いのですけど、たまに数値をベタに書いて、何となく小数にしちゃったりした時にぶちあたります。そんな時に「なんでだろう?」と思うのではなく「そういやそうだった」と思い出せれば、無駄に悩む時間が減らせられると思います。
assert はとにかく書いて、失敗させてみて、どう表示されるかを認識しておくべきだと思います。失敗した時、何故失敗したかわからないと意味がありません。デバッガとか使ったらたぶん負けです。
カスタムAssertion
assert の話が終わったので、カスタムAssertionの話をしても大丈夫だと思います。
実際のテストコードでは同等の検証が複数箇所で出て来ます。例えば、同じメソッドに対して様々な条件でのテストを行う場合などが該当します*2。同じ検証をするならば、それはまとめておくべきだと思います。複数行に渡るとそれだけでも読み辛いし、似たような検証なのか、まるっきり同じ検証なのかを見極める必要が出てきます。ちょっと違う場合など、意図して異なっているのか、単なるミスかは判断がつきません。これはテストコードでなくても同じことです。こういう時に使用するのが カスタムAssertion です。*3
うだうだ書いてますけど、簡単な話で「自分が欲しい assert メソッドを書く」だけです。単純な カスタムAssertion は assert が列挙されているだけになると思います。はじめは複数の assert をメソッド抽出する所から初めて良いんじゃないでしょうか。
こんな感じで。
import static org.junit.Assert.*; import org.junit.Test; import twitter4j.*; public class CusomAssertTest { @Test public void getTwitterUser_backpaper0() throws Exception { User actual = getUser("backpaper0"); assertTrue(actual.getDescription().contains("Java")); assertEquals("うらがみ", actual.getName()); assertEquals("大阪", actual.getLocation()); } @Test public void getTwitterUser_tan_go238() throws Exception { User actual = getUser("tan_go238"); assertEquals("Go Tanaka", actual.getName()); assertEquals("Kyoto-Osaka-Tokyo, Japan", actual.getLocation()); assertTrue(actual.getDescription().contains("Java")); } private User getUser(String screenName) throws TwitterException { Twitter twitter = TwitterFactory.getSingleton(); return twitter.showUser(screenName); } }
同じことを検証してるので、「assertUser」としてくくりだす。
import static org.junit.Assert.*; import org.junit.Test; import twitter4j.*; public class CusomAssertTest { @Test public void getTwitterUser_backpaper0() throws Exception { User actual = getUser("backpaper0"); assertUser(actual, "うらがみ", "大阪"); } @Test public void getTwitterUser_tan_go238() throws Exception { User actual = getUser("tan_go238"); assertUser(actual, "Go Tanaka", "Kyoto-Osaka-Tokyo, Japan"); } private void assertUser(User actual, String name, String location) { assertTrue(actual.getDescription().contains("Java")); assertEquals(name, actual.getName()); assertEquals(location, actual.getLocation()); } private User getUser(String screenName) throws TwitterException { Twitter twitter = TwitterFactory.getSingleton(); return twitter.showUser(screenName); } }
例が微妙なのであまり嬉しくないですが、項目数が多くなったり、テストメソッド数が増えたり、あとから項目を追加しなきゃいけなくなった場合とかにも地味に効いてくる筈です。
Matcher
JUnit4.4から assertThat が追加されました。この子は他の assert 達とはちょっと毛色が違いまして、今までの全ての assert を無かったことにしてしまうものです。半分くらい誇張表現ですが、半分くらいは本気で、私は全て assertThat で書いています。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()); } }
なんだか行数が多いですけど description は失敗時のメッセージを作ってるだけで、本体は !matcher.matches(actual) です。マッチしなければ AssertionError が発生する。それだけの単純なものです。
@Test public void testAssertThat() { String actual = "ABC"; assertThat(actual, is("ABC")); }
is は org.hamcrest.CoreMatchers のメソッドを static import したものです。このメソッドから返される実体は org.hamcrest.Core.Is になります。is の他にも色々な Matcher が定義されているのですが Is#is は Matcher を引数にとって繋ぐだけのお仕事もしてくれるので、だいたいここまでは固定です。このとき失敗時のメッセージに「is」とついたりします。
バージョンによっていくらか差異はありますが、JUnit4.10 (hamcrest1.1) では以下のメソッドが使えます。*4
- org.hamcrest.CoreMatchers
- allOf
- anyOf
- anything
- equalTo
- instanceOf
- is
- not
- notNullValue
- nullValue
- sameInstance
- org.junit.matchers.JUnitMatchers
- both
- containsString
- either
- everyItem
- hasItem
- hasItems
だいたい名前から意味は解ると思います。用途がかぶってるものもあります。例えば is と equalTo の区別がつかなかったりしますけど、実際 is の中では equalTo 呼んじゃってるので同じだったりします。これらの Matcher たちは引数に Matcher をとるものも多いです。使い方の詳細は……@daisuke_mさんが2年以上前に書かれていましたので、そちらを参考にしてください。大筋は変わって無いと思います。それっぽく書けばそれっぽく動きますし。
カスタムMatcher
さて、 assertThat で書いていると、今ある Matcher だけでは物足りなくなってきます。なので普通は Matcher を作りたくなることでしょう。Matcher インタフェースを直接実装しても良いのですが、だいたいは BaseMatcher を継承すると思います。BaseMatcher を継承した TypeSafeMatcher ってのがあったりするのですが、 org.junit.internal.matchers パッケージのは使わないほうが無難だと思います。internal だし、null の挙動が微妙だった記憶があります。(hamcrest1.2以降では org.hamcrest.TypeSafeMatcher にありますので、これは使って良いものだと思います。他にも abstract な Matcher が幾つか増えてる感じです。)
単純な Matcher を作る場合は BaseMatcher を継承し、2つのメソッドをそれなりにオーバーライドしておけばそれっぽく動きます。
- matches
- 検証の成功/失敗を boolean で返すだけ。
- describeTo
- 検証失敗時に expected に表示されるメッセージ。
ごくシンプルなものであれば、無名クラスで良いと思います。引数を受けたり多少複雑な検証を行うならクラスを作った方が良いです。押し込むと訳が分からなくなりますので。クラスを作って、static import するためのメソッドを作っておく感じ。Matcher を作る時はそのプロジェクト用の CoreMatchers みたいなクラスを作ってますね。
import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; import org.hamcrest.BaseMatcher; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.hamcrest.TypeSafeMatcher; import org.junit.Test; public class CustomMatcherTest { @Test public void testEmptyString() throws Exception { assertThat("AB", is(emptyString())); } @Test public void testLessThan() throws Exception { assertThat("ABD", is(lessThan("ABC"))); } // メソッドで作っちゃう public static Matcher<String> emptyString() { return new BaseMatcher<String>() { @Override public boolean matches(Object item) { if (item instanceof String) return ((String)item).isEmpty(); return false; } @Override public void describeTo(Description description) { description.appendText("empty String"); } }; } public static <T extends Comparable<T>> Matcher<T> lessThan(T expected) { return new LessThan<T>(expected); } } // クラスを作る class LessThan<T extends Comparable<T>> extends TypeSafeMatcher<T> { private final T expected; public LessThan(T expected) { this.expected = expected; } @Override public boolean matchesSafely(T actual) { return actual.compareTo(expected) < 0; } @Override public void describeTo(Description description) { description.appendText(" less than "); description.appendValue(expected); } }
Matcher を作れると言うことは、ちょっと凝った検証も出来てしまうと言うことになります。こうなると カスタムAssert とどちらを選択するかに悩むことになるかもしれません。私的には全部 Matcher で済ませたい衝動に駆られるのですが、assertThat 失敗時の got には単純な toString が出力されますので、対象に適切な toString が実装されていない場合などは失敗原因が分かり辛くて困ったりします。一応 Matcher では actual に手が届くので、メッセージを出せなくはないのですが、結果は少々歪な感じになってしまいます。こういう時は素直に カスタムAssert の方で「何の検証をしている時に失敗したか」を明確に出す方が、テストとしての価値は高いと思います。
Rule
JUnit4 から継承の呪縛から解き放たれたわけですが、テストに共通の処理がある場合、従来ならば TestCase を継承した HogeTestCase みたいなのを作って、皆でそれを継承してテストを書きましょう見たいなことをやっていたかもしれません。やっていなかったかもしれません。どちらにせよ、せっかく解かれた呪縛に自分からかかりにいく必要はありません。継承は要らなくなったので、使わない方向で。
継承せずに複数のテストで同じようなことをしたい場合にどうするかと考えると、簡単に思いつくのが共通の処理を static メソッドか何かで書いておいてそれを呼び出すというかたち。でもそれって柔軟性に欠けるし、テストに対して何かを出来るわけでは無い。そんなわけで Rule です。
Rule は @Rule を付けた TestRuleインタフェースを実装したフィールドを宣言しておくことで、そのテストクラス内の全メソッドに適用されるものです。簡単な例だとこんな感じ。
import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; public class RuleTest { @Rule public TestName testName = new TestName(); @Test public void testHoge() throws Exception { System.out.println(testName.getMethodName()); // ... } @Test public void testFuga() throws Exception { System.out.println(testName.getMethodName()); // ... } } /* 実行すると以下のようにメソッド名が出力される testHoge testFuga */
Rule は メソッドに対する @Rule と クラスに対する @ClassRule があります。どちらも同じ Rule が使えるのですが、例えばテストメソッド名を取得する TestName は @ClassRule だと何も設定されません。基本的にテストはメソッドごとに独立しているべきなので、「@ClassRule でないと駄目!」なんて事態は避けた方が良いと思います。使うとすれば、取得するのにコストがかかる外部リソースなどでしょうかね。
便利なのが ExpectedException です。これは名前の通り例外を期待するものです。ExpectedException を使用しない場合の例外の検証はこんな感じでした。
@Test(expected = RuntimeException.class) public void testExpected() throws Exception { throwRuntimeException("hoge"); } @Test public void testTryCatch() throws Exception { try { throwRuntimeException("hoge"); fail(); // 例外が出なかったときのため } catch (RuntimeException e) { assertThat(e, is(instanceOf(RuntimeException.class))); assertThat(e.getMessage(), is("hoge")); } }
これが以下のようになります。
@Rule public ExpectedException exception = ExpectedException.none(); @Test public void testExpectedException() throws Exception { exception.expect(RuntimeException.class); exception.expectMessage("hoge"); throwRuntimeException("hoge"); }
例外の内容がチェックできないexpectedの欠点も、try-catchの記述の面倒さと可読性の低さも補えています。とても便利。なお、ExpectedException#expect も ExpectedException#expectMessage も Matcher が使えます。例外用の カスタムMatcher を作っておけば、もう少しだけ楽出来ます。
@Rule public ExpectedException exception = ExpectedException.none(); @Test public void testExpectedException() throws Exception { exception.expect(runtimeException("hoge")); throwRuntimeException("hoge"); }
Rule で他にそのまま使えそうなのは Timeout くらいなものですが、そもそもタイムアウトのテストなんて書いたこと無いです…。
カスタムRule
標準の Rule はそのままではあまり役に立たないのですが、 Rule は各環境で適したものを作っていってこそ効果を発揮するものだと思います。と言うわけで Rule を作りましょう。
先に挙げた TestName は名前を取得するだけですが、これは TestWatcher の非常に簡単な実装例です。TestWatcher はテストメソッドの前後に幾つかのメソッドを実行するように作られていて、それらのうち必要なのをオーバーライドする形で使います。対象は以下の4つ。
- TestWatcher
- starting : テスト実行前
- succeeded : テスト実行後(成功時)
- failed : テスト実行後(失敗時)
- finished : テスト実行後(成功/失敗に関わらず)
これを見ると @Before/@After と役割がかぶっているように見えるかもしれません。実際かぶっています。@Before/@After が無くなることは無いと思いますが、テスト対象に密接に絡まない事前/事後処理ならば、Rule を使うことでテストコードをシンプルに出来ると思います。例えば前述の TestName を使った例が「テストメソッド名を書き出したい」のなら、Rule を使用しない場合は @Before に書きそうな所ですが、それを実装した Rule を適用してしまえば良いと言う感じです。
import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestRule; import org.junit.rules.TestWatcher; import org.junit.runner.Description; public class RuleTest { @Rule public TestRule testName = new TestWatcher() { protected void starting(Description d) { System.out.println(d.getMethodName()); }; }; @Test public void testHoge() throws Exception { // ... } @Test public void testFuga() throws Exception { // ... } }
Rule の実装は普通別クラスにすると思います。
Rule の中で私がよく使うのは ExternalResource です。これは TestWatcher の starting/finished だけを取り出して before/after とメソッド名を変えた感じです。そのまま @Before/@After に対応しそうですが、名前の通り外部リソースの管理をこれにやらせると言う手があります。よく使うのは、DBコネクションの管理をこれにやらせるからです。スローテストの原因にもなりますし、DBの絡むユニットテストは避けるべきなのですが、諸般の事情により避けられた試しがありません。なので苦肉の策としての Rule でDB接続したりしてます。TestName のコードからもわかる通り、Rule のインスタンスから値を取り出したり、逆に値を設定したりも出来ます。