JUnit4基礎+Hamcrestや例外検証辺りまで
なんでいまさら4なのかというツッコミは置いといて。色々忘れるしまとめておくメモ書き。
JavaのテストフレームワークJUnitの一般的な使い方あたりからhamcrestとExpectedExceptionあたりまで語れれば。
AssertJとか入ってくるとまた変わるけどまあなんとかなるでしょ。
対象
- JUnit触ったことあるけど細かいことは覚えてない
assertEquals
なら知ってるけどassertThat
は知らないassertThat
はis
と書くためだけに使うException
はtry-catch
とfail()
を書いてテストするもの@Test(expected = Xxx.class)
は使うけど中のメッセージとかも試験したい場合どうすんの?- なんか
@Rule
で例外検証できるみたいだけどよくわからん
JUnit基礎(個人的な好み含む)
テストクラスは専用のソースフォルダに入れる
大体プロジェクト構成はテンプレでこうなるはず。
テストクラスはテスト対象と同じパッケージに原則[テスト対象クラス名+Test]の名称でクラスを切る
Eclipseがデフォでこんな風にしたり、慣習的な面があったり。
パッケージ合わせるのはpackage-privateやprotected直接呼べる観点からもやっときたい。
クラス名はEclipseでテスト対象-テストケース間を対応して開くショートカットなどこれを前提とした機能もあったりする。
TestCaseクラスを継承しない
extends TestCase
が必要だったのはJUnit3のお話。JUnit4以降のテストは別の手段でテストケース動かしてる。
共通の前処理や後処理は@Before, @Afterを使う
JUnit3のsetUp
とtearDown
に相当する奴ら。共通処理が不要ならあえて定義する必要はない。
Mockとか利用しだすと大抵必須で登場することになる。
public class BasicResultsTest{ @BeforeClass public static void setUpClass() { // テスト全体の開始前に一度実行 } @Before public void setUp() { // テストケース毎の開始前に実行 } @After public void tearDown() { // テストケース毎の終了後に実行 } @AfterClass public static void tearDownClass() { // テスト全体の終了後に一度実行 } }
この例でテストケースが2つ定義されていたとすると下記の順序で実行される。
setUpClass
→setUp
→テストケースA
→tearDown
→setUp
→テストケースB
→tearDown
→tearDownClass
各テストケースはthrows Exceptionを宣言する
必須じゃないけど例外検証する場合は必要になる場合も多いし、一律やってもいいんじゃないかという話。
テストケースのメソッド名は[test+対象メソッド名]に日本語も利用してテストパターンの説明も加える(好みレベル)
慣習+α。メソッド名がtestで開始するのは必須じゃないけど、なんとなくこうしたくなる。
日本語は意見分かれるけど、テストケース読むときや実行結果見た時に何が行われているか直感的にわかるので積極利用推奨派。
メソッド名で説明しきれない複雑なパターンはjavadocやコメントで補足。
@Test public void testHogeMethod_処理が成功したら文字列が返却されること () throws Exception { // testHogeMethodごにょごにょのテストケースでhogeMethodを検証する String result = testTarget.hogeMethod(); assertThat(result, is("fugafuga")); }
assertion、verify、例外検証が無いテストは書かない
いや当然なんだけど、どっかの誰かが作ったソースをメンテナンスするとassertEquals
もverify()
も例外検証も書かれてなかったりするのです…
大抵「カバレッジを通すこと」や「テストケースを書くこと」が目的化された残念パターン。
Hamcrest
Hamcrestって何よ?
単純なJUnitの書き方だと、assertEquals
, assertTrue
, assertNull
あたりで検証を頑張るわけだけど、世の中そんな単純じゃない。
- 文字列の前方一致とか部分一致とか取りたい。
- いくつかの条件の組み合わせで検証したい。
- リストの中を見て特定の値が含まれていることを確認したい(他の値はなんでもいい)。
- etc
ということで、そんな要望に答えたassertion用のライブラリがあって、代表的なものの一つがHamcrestである。assertThat
とか出てきたら使われてるかも。
使うと決めたらassertEquals
やassertNull
は忘れて一本化するくらいのほうが統一感は取れると思う。
一箇所assertEquals
が現れた所で読めなくはないから併用してもいいけどね。
Eclipseで使う時の準備
ライブラリ引きこむのは当然として、この手のライブラリはスタティックインポートできるようにしときましょう。
Eclipseの設定メニューからJava
→エディター
→コンテンツ・アシスト
→お気に入り
下記を設定しとく。
- org.hamcrest.Matchers
- org.hamcrest.MatcherAssert
- org.hamcrest.CoreMatchers
※ Mockitoと一緒に使うとスタティックインポートしたメソッド名が被ったりするのがイマイチ…
基本の検証
値の一致を検証したりとか、Null検証したりとか。
@Test public void testHamcrestSample_ただのサンプル () throws Exception { // 結果取得とかしてから… // 特定の数値であること assertThat(result, is(3)); // 特定の文字列であること assertThat(result, is("期待する文字列")); // true/falseであること assertThat(result, is(true)); // 大小比較する assertThat(result, greaterThan(2)); assertThat(result, greaterThanOrEqualTo(3)); assertThat(result, lessThan(4)); assertThat(result, lessThanOrEqualTo(4)); // nullであること(isはなくてもいけるけどdocとか見る限りこれが正しいのかな) assertThat(result, is(nullValue())); // nullではないこと assertThat(result, is(notNullValue())); }
ちなみにassertEquals
の引数は期待値, 検証値の順序だが、assertThat
の場合は検証値, 期待値(検証方法)の順序で登場する違いがある。
assertEquals
の順序は入れ替えてもそれっぽく検証できるけど、テスト失敗したときにエラーメッセージが歪む。
文字列の検証
接頭-接尾辞や部分文字列検証。正規表現検証もあれば便利そうなものの、動作確認バージョンでは存在しなかった。
@Test public void testHamcrestSample_ただの文字列サンプル () throws Exception { // 特定の部分文字列を含む assertThat("対象に部分文字列がついている", containsString("部分文字列")); // 特定の文字列で開始する assertThat("接頭辞がついた検証対象", startsWith("接頭辞")); // 特定の文字列で終了する assertThat("検証対象に接尾辞", endsWith("接尾辞")); // 連続する空白文字などは除いて検証(文章の一致とかを見たい場合が主か) // 連続する空白や改行文字などの違いは許容されるけど、そもそも空白がある場合 vs ない場合で検証したらエラーになる assertThat("この番組は ご覧のスポンサーの提供で\t\tお送りしました\r\n", equalToIgnoringWhiteSpace("この番組は ご覧のスポンサーの提供で お送りしました")); }
条件の組み合わせなど(論理演算的なやつら)
AndとかOrとか否定(Not)とか
@Test public void testHamcrestSample_ただの条件サンプル () throws Exception { // OR:いずれかの条件を満たせば良い // either-orは自然な英語っぽいけど2個目で打ち止めなので、それ以上ならanyOf // 微妙にわかりにくいけどeitherはメソッドの結果からorを呼ぶ形 assertThat("接頭辞がついた検証対象に接尾辞", either(startsWith("接頭辞")).or(endsWith("suffix"))); assertThat("接頭辞がついた検証対象に接尾辞", anyOf(startsWith("prefix"), containsString("検証対象"), endsWith("suffix"))); // AND:複数の条件を同時に満たす必要がある // both-andは自然な英語っぽいけど2個目で打ち止めなので、それ以上ならallOf // bothの書き方は前述のeitherみたいになってる assertThat("接頭辞がついた検証対象に接尾辞", both(startsWith("接頭辞")).and(endsWith("接尾辞"))); assertThat("接頭辞がついた検証対象に接尾辞", allOf(startsWith("接頭辞"), containsString("検証対象"), endsWith("接尾辞"))); // NOT:条件を否定する assertThat("接頭辞がついた検証対象に接尾辞", not(containsString("ありえへん検証対象"))); }
anyOf
やallOf
を2個の条件に使って全体統一してもいいかもしれない。
Collectionなどの検証
Collection内に特定の値を含んでいれば良いケースや、全要素に対して共通検証したい場合など
@Test public void testHamcrestSample_ただのCollectionサンプル () throws Exception { List<String> result = Arrays.asList("検証文字列1", "検証文字列2", "検証文字列3"); // 特定の値が含まれることの確認 assertThat(result, hasItem("検証文字列2")); // 複数もいける assertThat(result, hasItems("検証文字列2", "検証文字列3")); // hasItemに他のMatcherを噛ませることもできる assertThat(result, hasItem(endsWith("文字列1"))); // Matcher噛ませても複数いける…のだがジェネリクス関連で警告が出た assertThat(result, hasItems(endsWith("文字列1"), containsString("文字列2"))); // すべての要素に対して共通に検証する assertThat(result, everyItem(containsString("検証文字列"))); }
他は?
CoreMatchersとMatchersのソースやjavadoc見れば色々載ってる。
なんか一部の検証メソッドがないんだけど…
ライブラリが古いか、hamcrest-coreだけ引きこまれているような事態になってないか。
Mavenなどを使ってると依存関係で勝手に意図しないものが引きこまれてたりします。
- hamcrestのバージョン → 書いた時点では1.3が良さげ。それ未満なら上げてみる。
- hamcrest-coreだけしかない → hamcrest-allを入れてみる。
- ライブラリの依存関係で勝手にhamcrestが引きこまれていないか見る → ありそうなら必要に応じてexcludeする。
例外検証
try-catchして正常系をfailさせるんじゃないの?
基本的に必要は無くなったので新しい書き方は覚えましょう。
@Test(expected)での例外検証方法は?
アノテーションに期待する例外クラスを指定することができる。
テストケースから例外をそのままthrowすることで、アノテーション定義に従った例外がthrowされたかどうか勝手に検証される。
// IllegalArgumentExceptionがメソッドからthrowされなかったら失敗する @Test(expected = IllegalArgumentException.class) public void testHamcrestSample_ただの例外検証サンプル () throws Exception { throw new IllegalArgumentException("テスト例外"); }
ExpectedExceptionって何よ?
前述の通り、JUnit4では@Test(expected)
でtry-catchを書かなくても例外検証ができるようになった。
とはいえ、これで検証できるのは例外がthrowされたこと+その型だけでmessageやcause(場合によってはその他プロパティ)を検証することができない。
そういった場合に、例外詳細を検証できる書き方が導入されたのがこいつである。
ExpectedExceptionの書き方
ほぼほぼ外部参考…
ExpectedException ルールを使いこなしたい - Qiita
// publicフィールドで定義が必須 // 例外発生を期待しないテストのため、ExpectedException.none() を設定しておく @Rule public ExpectedException expectedException = ExpectedException.none(); @Test public void testExpectedException() { // 投げられる例外のクラスを以下のように検証可能 expectedException.expect(IllegalStateException.class); // 例外に対してMatcherで検証させることも expectedException.expect(hasProperty("message", is("異常事態"))); // 例外のメッセージは以下のように検証可能 expectedException.expectMessage("異常事態"); // 例外のメッセージにMatcherで検証もできる expectedException.expectMessage(startsWith("異常")); // Caused By の例外オブジェクトの検証には expectCause() を使う expectedException.expectCause(allOf( // Caused By の例外クラスを以下で指定する instanceOf(NullPointerException.class), // 例外メッセージをhasPropertyで確認 hasProperty("message", is("ぬるぽ")))); // 事前にexpectを設定して、その後に例外をthrowする処理が呼び出されるようにする throw new IllegalStateException("異常事態", new NullPointerException("ぬるぽ")); }
詳細を検証する必要がないテストケースでは@Test(expected)
で書いて併用するのがいい気がする。
ExpectedExceptionでcauseのcauseはどう検証するの?
ざっと調べてみた限りわからんかった…その検証が必要な場合は設計を見なおしたほうがいい気もする。
最悪古き良きtry-catchを行えば検証できる。
その他のテストの話題
今後頑張れるならまとめてみる。
- Mockライブラリ(mockitoかな)
- そもそもなんで必要なのさ
- mockって何?
- spyって何?
- verifyって何?
- argument captureって何?
- AssertJ
- Truth
- JUnit5のこと
- 色々な@Ruleのこと
- パラメタライズテスト