JUnit実践入門 4日目(テストコードの書き方について~ アサーション)

JUnitが提供するアノテーション

■@Test -テストメソッドを宣言する
Testアノテーションを付与することによって、JUnitが実行するテストメソッドを認識する。

付与される属性
・expected -例外の早出を検証するテストで、早出が期待される例外クラスを指定するために利用する。

@Test(expected = IllegalArgumentException.class){
 sut.doSomething();
}

・timeout -ユニットテストタイムアウト値を設定するために利用する。設定した時間以上の時間がかかった場合、テストは自動的に失敗するようになる。

@Test(timeout = 100L)
public void timeoutTest() throws Exception{
 sut.doSomething();
}

■@Ignore -テストの実行から除外する
原則として、ユニットテストは常に全てを実行し、成功するようにすべき。だが、都合により一時的にテストの実行を抑制したい場合に@Ignoreを付与することで対象のメソッドの実行をスキップすることができる。なお、テスト区タスにも付与でき、その場合はクラス内のメソッド全てのメソッドの実行がスキップされる。

@Ignore("未実装のためスキップ")
@Test
public void divideに4と2を与えると2を返す() throw Exception{
 //Exercise
 int actual = sut.divide(3, 4);
 //Verify
 assertThat(actual, is(7));
}

■@Before -テスト実行前に処理を行う
JUnitによるユニットテストでは、テスト対象のクラスごとにテストクラスを作成し、テストクラスに複数のテストメソッドを定義する。
その場合、メソッド間で重複した初期化コードが発生する。重複した初期化コードをメソッドに抽出し、@Beforeアノテーションを付与することによって重複を削除することができる。@Beforeアノテーションを用いたメソッドは慣習としてsetUpとする。

@Before
public void setUp() throw Exception{
 sut = new Calculator();
 sut.init();
}

※@Beforeによる初期化処理の共通化はクラス内にしか適用できないため、クラス間で共通処理を適用したい場合にはルールを利用する。

■@After -テストの実行後に処理を行う
テストケースに共通する後処理をメソッドとして抽出する。@Afterに定義された終了処理はテストの成功/失敗にかかわらすか鳴らす実行されるのでリソースの解放など「必ず行わなければならない後処理」が必要な場合に利用される。慣習としてメソッド名はtearDownとする。

@After
public void tearDown() throw Exception{
 sut.shutdown();
}

基本的なアノテーションとしては上記4点となる。

JUnitのテストパターンについて
ユニットテストは比較的パターン化しやすい。パターンを構築することで、ユニットテストを効率よく書くことができる。
下記3パターンはその一例となる。

■標準的な振る舞いを検証するテスト
ほとんどのユニットテストはこのパターンとなり、初期化/実行/検証/後処理の4つのフェーズで構成される(4フェーズテスト)
すなわちテスト対象オブジェクトや入力/検証に必要なデータを初期化し、テスト対象となるメソッドを一つだけ実行したあと、期待される結果となるかを検証する。
標準的な振る舞いを検証するテストでは、検証時に予期された値を実際の値が異なる場合にテスト失敗となる。
またテスト実行中に何らかの例外が発生した場合もテスト失敗となる。

■例外の送出を検証するテスト
メソッドの実行時に例外が送出されることを検出するテスト。expect属性に指定した例外が送出された場合成功となる。
注意点としてJUnitの例外の検証では、例外の型以外はチェックできない。したがって、例外に含まれるメッセージの検証や、想定しない箇所で同じ型の例外については検証できない。

■コンストラクタを検証するテスト
インスタンスが生成された時に、初期状態として妥当な値が設定されているかを検証する。
コンストラクタをテストする場合には、インスタンスの生成が実行フェーズに相当する。
つまり、インスタンスはSUTでもあり、実測値でもある。変数名をSUTでも良いが、instanceにすると理解しやすい。

@Test
public void インスタンス化テスト(){
 //Exercise
 Person instance = new Person("Duke");

 //Verify
 assertThat(instance.getName(), is("Duke"));
 assertThat(instance.getAge(), is(-1));
 assertThat(instance.getEmail(). is(nullValue()));
}

アサーション:実行して得られた結果が期待される結果と一致するかどうかの検証。比較検証。
■assertThat
JUnitではassertThatメソッドとMatcherAPIを利用し、アサーションを記述する。

public static <T> void assertThat(T actual, Matcher<? super T> matcher)

Matcherオブジェクトは、equalsメソッドによる比較で、実測値と期待値が等しいことを検証する。
基本的にはassertThatメソッドを用いて検証を行う。

■fail
Assertクラスのfailメソッドは、無条件にそのテストを失敗させる。
何らかの理由でテストを失敗させたい場合に利用する。

@Test
public void 何か難しいけど重要なテストケース(){
 fail("TODO テストコードを実装する");
}

Matcher APIのメソッド
https://junit.org/junit4/javadoc/4.11/org/hamcrest/CoreMatchers.html

■is
オブジェクトクラスのequalsメソッドによる比較を行う。
ユニットテストで比較検証を行う時、多くのケースはこのisで実施する。
ただし、isメソッドは型パラメータを持つメソッドであるため、nullを指定するとコンパイルエラーとなる。

assertThat(actual,is(null));

実測値がnullであることを確認する場合には下記のnullValueメソッドを利用する。

■nullValue
nullであることを検証するMatcherを返す。
単体でも利用できるが、通常はisメソッドと組み合わせて利用する

assertThat(actuals,is(nullValue()));

■not
他のMatcherの評価値を反転させるMatcherを返す。
nullValue同様に、isメソッドと組み合わせて利用する。

assertThat(actual,is(not(0)));

■notNullValue
nullでないことを比較検証するMatcherを返す。

assertThat(actual,is(notNullValue()));

// これと同等の意味
assertThat(actual,is(not(nullValue)));


■sameInstance
実測値と期待値が同一のインスタンスであるかを比較するMatcherを返す。
つまり、==演算子を用いた比較を行う。

assertThat(actual,is(sameInstance(expected)));

なお、プリミティブ型でsameInstanceメソッドを利用する場合、ボクシング変換されたラッパー型のオブジェクトに対してインスタンスの同一性を比較するため注意。

■instanceOf
実測値が期待するクラスのインスタンスと互換性のある型であるかを比較判定するMatcherを返す。
instanceof演算子を用いた比較を行い、オブジェクトの型を検証する。

assertThat(actual,is(instanceOf(serializable.class)));

JUnitMatchersのメソッド
https://junit.org/junit4/javadoc/4.12/org/junit/matchers/JUnitMatchers.html

■hasItem
リストや配列など反復可能なオブジェクト(実測値)に期待する値が含まれているかを判定するMatcherを返す。
この時、順序や他の要素は無視し、実測値のリストの中に指定した要素が含まれていることを検証する。

List<String> actual = sut.getList();
assertThat(actual,hasItem("world"));

■hasItems
リストや配列など反復可能なオブジェクト(実測値)に期待する値が、複数含まれているかを判定するMatcherを返す。
必要な要素が全て含まれているかどうかを検証したい場合に有効なMatcherである。

List<String> actual = sut.getList();
assertThat(actual,hasItems("Hello", "world"));

カスタムMatcherの作成
プロジェクト固有の検証やより柔軟な検証を行いたい、詳細なエラーメッセージをレポートしたい場合にカスタムMatcherを作成すると便利。

作成手順
org.hamcrest.Matcherインターフェースを実装したクラスとして作成する。
※ただしMatcherインターフェースを直接インプリメントすることは推奨されておらず、org.hamcrest.BaseMatcherクラスのサブクラスとする。

public abstract class BaseMatcher<T> implements Matcher<T> {

    /**
     * @see Matcher#_dont_implement_Matcher___instead_extend_BaseMatcher_()
     */
    @Override
    @Deprecated
    public final void _dont_implement_Matcher___instead_extend_BaseMatcher_() {
        // See Matcher interface for an explanation of this method.
    }

    @Override
    public void describeMismatch(Object item, Description description) {
        description.appendText("was ").appendValue(item);
    }

    @Override
    public String toString() {
        return StringDescription.toString(this);
    }
}