概要
JUnitにおいて、値の検証を担う要素である
「アサーション」、「Matcher API」について理解する。
アサーションとは
アサーション(Assertion)とは、直訳で断言、断定という意味を持つ。
JUnitは大きく分けると、
- 前提条件、想定結果の定義
- テスト対象の実行
- 想定結果と実行結果の比較検証
上記3つのステップでユニットテストを行う。
JUnitにおけるアサーションとは、
3つ目の「想定結果と実行結果の比較検証」を行うための仕組みである。
実際のコードでは、以下のように使用する。
int expected = 1;
int actual = calc();
assertThat(actual, is(expected));
このように記述することで、テストを
Assert that actual is expected.(実測値は想定値だと断言する。)
のように自然言語(話し言葉の英語)で書くことができる。
Matcher APIとは
先ほど、例に挙げたソースコードに登場したis(expected)
というメソッドは、
Matcher APIのCoreMatcherクラスに実装されているstaticメソッドである。
Matcher APIとは、値の比較方法を提供するアサーションに対し、
値の検証方法を提供する仕組みであり、
以前はHamcrestというJUnitの拡張ライブラリのみに実装されていたが、
現在ではJUnit本体にも組み込まれている。
CoreMatchersが提供するMatcher
is 「actual は expected である。」
値が同値であることを検証する。
assertThat(actual, is(expected));
not 「actual は expected でない。」
Matcherが返す検証結果を反転させる。is以外のMatcherにも適用可能である。
assertThat(actual, is(not(expected)));
nullValue 「actual は null である。」
isメソッドは型パラメータを持つメソッドなのでnull値を渡すことができない。
よって、null検証を行いたい場合はnullValueメソッドを使用する。
assertThat(actual, is(nullValue()));
/*
以下の書き方も可能だが、
より自然言語に近い書き方をするため上記の書き方をするのが通例
*/
assertThat(actual, nullValue());
notNullValue 「actual は null でない。」
値がnullでないことを検証する。
assertThat(actual, is(notNullValue()));
// 以下と同義
assertThat(actual, is(not(nullValue()));
sameInstance 「actual と expected は 同一のインスタンス である。」
値が同値であることを検証する。
isメソッドがequals
による比較なのに対して、sameInstanceは==
による比較を行う。
assertThat(actual, is(sameInstance(expected)));
instanceOf 「actual は expected と互換性のある値である。」
値が想定する値を継承していることを検証する。
assertThat(actual, is(instanceOf(Serializable.class)));
JUnitMatchersが提供するMatcher
hasItem 「actual は expected を含んでいる。」
コレクションクラスのように反復可能な(イテレーターを持っている)実測値に
想定する値が含まれていることを検証する。
List<String> actual = getList();
assertThat(actual, hasItem(expected));
hasItems 「actual は expected を含んでいる。」
検証する内容はhasItemと同じだが、想定値に可変長引数をとる点が異なる。
順序に関係なく、想定する値すべてが含まれているかを検証するのに有効なMatcher。
List<String> actual = getList();
assertThat(actual, hasItem(expected1, expected2));
補足:hamcrest-libraryが提供するMatcher
その他、hamcrest-libraryという拡張ライブラリは
コレクション、数値、テキストなど汎用的な検証に使用できるMatcherを提供している。
他にも様々な拡張ライブラリがあるため、次に紹介するカスタムMatcherの導入の前には、
すでに提供されているMatcherがないか探してみるのが吉。
カスタムMatcher
Matcherは、独自の検証を行うカスタムMatcherというものを作ることができる。
ここでは、以下の要件を満たすMatcherを独自に実装する。
・Dateクラスを比較検証する時、年、月、日までを比較対象とする
・想定値と実測値を以下のフォーマットで出力する
フォーマット:is "想定値(yyyy/mm/dd)" but actual is "実測値(yyyy/mm/dd)"
以下は、上記要件を満たしたMatcherの実装とその呼び出し処理。
import static jp.sample.matcher.IsDate.*;
~省略~
assertThat(new Date(), is(dateOf(2020, 4, 12)));
// 1. Matcherクラスの宣言
public class IsDate extends BaseMatcher<Date> {
// 3. 想定値を保持する仕組みの実装
private final int yyyy;
private final int mm;
private final int dd;
Object actual;
// 3. 想定値を保持する仕組みの実装
IsDate(int yyyy, int mm, int dd) {
this.yyyy = yyyy;
this.mm = mm;
this.dd = dd;
}
// 4. 検証処理の実装
@Override
public boolean matches(Object actual) {
this.actual = actual;
if (!(actual instanceof Date)) {
return false;
}
Calendar cal = Calendar.getInstance();
cal.setTime((Date) actual);
if (yyyy != cal.get(Calendar.YEAR)) {
return false;
}
if (mm != cal.get(Calendar.MONTH)) {
return false;
}
if (dd != cal.get(Calendar.DATE)) {
return false;
}
return true;
}
// 5. エラーメッセージの実装
@Override
public void describeTo(Description desc) {
desc.appendValue(String.format("%d/%02d/%02d", yyyy, mm, dd));
if (actual != null) {
desc.appendText(" but actual is ");
desc.appendValue(
new SimpleDateFormat("yyyy/MM/dd").format((Date) actual));
}
}
// 2. 検証メソッドの実装
public static Matcher<Date> dateOf(int yyyy, int mm, int dd) {
return new IsDate(yyyy, mm, dd);
}
}
1. Matcherクラスの宣言
カスタムMatcherの実装クラスとして、org.hamcrest.Matcherインターフェースを
インプリメントするが、Matcherインターフェースの直接のインプリメントは非推奨である。
そこで、Matcherインターフェースをインプリメントしている
org.hamcrest.BaseMatcherを継承する形で実装を行う。
BaseMatcherには、実測値を型パラメータとして持たせる。
/*
public class [任意のクラス名] extends BaseMatcher<[実測値の型]> {
}
*/
public class IsDate extends BaseMatcher<Date> {
}
2. 検証メソッドの実装
assertThatメソッドの第2引数に渡すメソッドを実装する。
第2引数には想定値が渡されるため、Matcherクラスのコンストラクタを定義して
Matcherクラスを想定値で初期化する。
想定値を保持する仕組みは「3. 想定値を保持する仕組みの実装」参照。
/*
public static Matcher<[実測値の型]> [任意のメソッド名]([初期化に必要な値]) {
return [Matcherクラスのコンストラクタ];
}
*/
public static Matcher<Date> dateOf(int yyyy, int mm, int dd) {
return new IsDate(yyyy, mm, dd);
}
3. 想定値を保持する仕組みの実装
値の検証には、実測値と想定値が必要となる。
そこで、Matcherクラスではそれらをメンバとして保持する仕組みを実装する。
※想定値はコンストラクタでセットするが、実測値はassertThatメソッドの中で
matchesメソッドが呼ばれる時に渡されてくるのでコンストラクタではセットしない。
// 想定値を保持するメンバ
private final int yyyy;
private final int mm;
private final int dd;
// 実測値を保持するメンバ
Object actual;
// 想定値をセットするコンストラクタ
IsDate(int yyyy, int mm, int dd) {
this.yyyy = yyyy;
this.mm = mm;
this.dd = dd;
}
4. 検証処理の実装
assertThatメソッドが実行されると、処理の過程で
Matcherインターフェースに定義されているmatchesメソッドが呼ばれる。
matchesメソッドで戻り値がfalseだとテスト失敗、trueだとテスト成功と判断される。
実測値を引数に取るため、Matcherクラスの保持している想定値と検証を行い、
想定通りの検証結果ならtrue、異なる場合はfalseを返却するように実装する。
@Override
public boolean matches(Object actual) {
// 渡された実測値をMatcherクラスのメンバとして保持する
this.actual = actual;
// 型チェック(お約束的に書く)
if (!(actual instanceof Date)) {
return false;
}
// 以下は独自実装のため、目的に合わせた実装をする
Calendar cal = Calendar.getInstance();
cal.setTime((Date) actual);
if (yyyy != cal.get(Calendar.YEAR)) {
return false;
}
if (mm != cal.get(Calendar.MONTH)) {
return false;
}
if (dd != cal.get(Calendar.DATE)) {
return false;
}
return true;
}
5. エラーメッセージの実装
matchesメソッド同様、Matcherインターフェースに定義されているメソッド。
テスト結果が失敗だった時のみ呼ばれ、ここではエラーメッセージの定義を行う。
@Override
public void describeTo(Description desc) {
// 想定値の出力
desc.appendValue(String.format("%d/%02d/%02d", yyyy, mm, dd));
// 実測値の出力
if (actual != null) {
desc.appendText(" but actual is ");
desc.appendValue(
new SimpleDateFormat("yyyy/MM/dd").format((Date) actual));
}
}
java.lang.AssertionError:
Expected: is "2011/02/10" but actual is "2012/03/08"
got: <Thu Mar 08 23:02:49 JST 2012>
エラーメッセージの雛形
[エラークラス名]
Expected: is [describeToメソッドで定義したメッセージ]
got: [実測値のtoString出力結果]
appendValueメソッド
// 引数に文字列を渡した場合
"2012/03/08" (ダブルコーテーションで囲まれた値)
// 引数にオブジェクトを渡した場合
<Thu Mar 08 23:02:49 JST 2012> (渡したオブジェクトのtoString出力結果)
appendTextメソッド
but actual is (ダブルコーテーションで囲まれない)
参考文献
この記事は以下の情報を参考にして執筆しました。