雑多に技術メモと他色々

主に自分用な技術メモが多くなる気がする。他色々が書かれるかどうかは不明。

JUnit4基礎+Hamcrestや例外検証辺りまで

なんでいまさら4なのかというツッコミは置いといて。色々忘れるしまとめておくメモ書き。
JavaのテストフレームワークJUnitの一般的な使い方あたりからhamcrestとExpectedExceptionあたりまで語れれば。
AssertJとか入ってくるとまた変わるけどまあなんとかなるでしょ。

対象

  • JUnit触ったことあるけど細かいことは覚えてない
  • assertEqualsなら知ってるけどassertThatは知らない
  • assertThatisと書くためだけに使う
  • Exceptiontry-catchfail()を書いてテストするもの
  • @Test(expected = Xxx.class)は使うけど中のメッセージとかも試験したい場合どうすんの?
  • なんか@Ruleで例外検証できるみたいだけどよくわからん

検証環境

Eclipsemaven使ってhamcrest引き込んでJUnit書く。
JUnitはExpectedExceptionが取り入れられた以降の4系で。

JUnit基礎(個人的な好み含む)

テストクラスは専用のソースフォルダに入れる

大体プロジェクト構成はテンプレでこうなるはず。

テストクラスはテスト対象と同じパッケージに原則[テスト対象クラス名+Test]の名称でクラスを切る

Eclipseがデフォでこんな風にしたり、慣習的な面があったり。
パッケージ合わせるのはpackage-privateやprotected直接呼べる観点からもやっときたい。
クラス名はEclipseでテスト対象-テストケース間を対応して開くショートカットなどこれを前提とした機能もあったりする。

TestCaseクラスを継承しない

extends TestCaseが必要だったのはJUnit3のお話。JUnit4以降のテストは別の手段でテストケース動かしてる。

共通の前処理や後処理は@Before, @Afterを使う

JUnit3のsetUptearDownに相当する奴ら。共通処理が不要ならあえて定義する必要はない。
Mockとか利用しだすと大抵必須で登場することになる。

public class BasicResultsTest{
	@BeforeClass
	public static void setUpClass() {
		// テスト全体の開始前に一度実行
	}
	
	@Before
	public void setUp() {
		// テストケース毎の開始前に実行
	}
	
	@After
	public void tearDown() {
		// テストケース毎の終了後に実行
	}
	
	@AfterClass
	public static void tearDownClass() {
		// テスト全体の終了後に一度実行
	}
}

この例でテストケースが2つ定義されていたとすると下記の順序で実行される。
setUpClasssetUpテストケースAtearDownsetUpテストケースBtearDowntearDownClass

各テストケースはTestアノテーションで修飾したpublic voidメソッドにする

JUnit4からは@Testアノテーションでテストケース用のメソッドを示す。

各テストケースはthrows Exceptionを宣言する

必須じゃないけど例外検証する場合は必要になる場合も多いし、一律やってもいいんじゃないかという話。

テストケースのメソッド名は[test+対象メソッド名]に日本語も利用してテストパターンの説明も加える(好みレベル)

慣習+α。メソッド名がtestで開始するのは必須じゃないけど、なんとなくこうしたくなる。
日本語は意見分かれるけど、テストケース読むときや実行結果見た時に何が行われているか直感的にわかるので積極利用推奨派。
メソッド名で説明しきれない複雑なパターンはjavadocやコメントで補足。

	@Test
	public void testHogeMethod_処理が成功したら文字列が返却されること () throws Exception {
		// testHogeMethodごにょごにょのテストケースでhogeMethodを検証する
		String result = testTarget.hogeMethod();
		assertThat(result, is("fugafuga"));
	}

assertion、verify、例外検証が無いテストは書かない

いや当然なんだけど、どっかの誰かが作ったソースをメンテナンスするとassertEqualsverify()も例外検証も書かれてなかったりするのです…
大抵「カバレッジを通すこと」や「テストケースを書くこと」が目的化された残念パターン。

Hamcrest

Hamcrestって何よ?

単純なJUnitの書き方だと、assertEquals, assertTrue, assertNullあたりで検証を頑張るわけだけど、世の中そんな単純じゃない。

  • 文字列の前方一致とか部分一致とか取りたい。
  • いくつかの条件の組み合わせで検証したい。
  • リストの中を見て特定の値が含まれていることを確認したい(他の値はなんでもいい)。
  • etc

ということで、そんな要望に答えたassertion用のライブラリがあって、代表的なものの一つがHamcrestである。assertThatとか出てきたら使われてるかも。
使うと決めたらassertEqualsassertNullは忘れて一本化するくらいのほうが統一感は取れると思う。
一箇所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("ありえへん検証対象")));
	}

anyOfallOfを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のこと
  • パラメタライズテスト