読者です 読者をやめる 読者になる 読者になる

倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

JUnit 4 のアサーション

JUnit テスト

拙者、未だに JUnit 3.x 系のテストをしてるので、ちょっと 4.x 系のテストコードに慣れようと 4.x 系で使えるアサーション方法を調査。 3.x から 4.x に移行する場合は

  • org.junit.Assert クラス

に定義されている static メソッドを使うのがスムーズなんだったっけ。 他に、昨今の DSL ブーム(?)に影響を受けたような(実際にはどうか知らないけど)、英文風に記述できる

  • Assert#assertThat() メソッドと org.hamcrest.Matcher<T> オブジェクト

による方法もあるようですね。 この記事ではそれぞれの場合のアサーション方法の列挙と簡単なサンプルを見ていきます。 テストの実行方法に関しては触れていませんので、IDE などを使わない場合は参考 URL の「JUnit4をやってみよう」の記事を参考にしてください。

参考 URL

org.junit.Assert クラスに定義されている static メソッド

まずは org.junit.Assert クラスに定義されている static メソッドを見ていきます。 プリミティブ型のために多量にオーバーロードされているメソッドなどもあって最初は面食らいましたが、列挙してみれば何てことはなさそうですん。 一覧:

メソッド アサーションが通る条件
assertTrue(boolean)
assertFalse(boolean)
引数が true
引数が false
assertNull(Object)
assertNotNull(Object)
(引数) == null
(引数) != null
assertSame(Object expected, Object actual)
assertNotSame(Object unexpected, Object actual)
(第1引数) == (第2引数)
(第1引数) != (第2引数)
assertEquals(long expected, long actual)
assertEquals(double expected, double actual, double delta)
assertEquals(Object expected, Object actual)
(第1引数) == (第2引数)
Math.abs(expected - actual) < delta
(第1引数).equals( (第2引数) )
assertArrayEquals(byte[ ] expecteds, byte[ ] actuals)
assertArrayEquals(short[ ] expecteds, short[ ] actuals)
assertArrayEquals(char[ ] expecteds, char[ ] actuals)
assertArrayEquals(int[ ] expecteds, int[ ] actuals)
assertArrayEquals(long[ ] expecteds, long[ ] actuals)
assertArrayEquals(Object[ ] expecteds, Object[ ] actuals)
2つの配列の要素が順序通りに全て等しい
assertThat(T actual, Matcher matcher) 第1引数を主語として、第2引数の述語を満たす
fail() 常に失敗

各メソッドの返り値は void なので省略しました。 非推奨のメソッドも省略。 assertSame() メソッドで「==」による評価、くらいが注意点でしょうか。 JUnit4.4 あたりから導入されたらしい assertThat() メソッドは次節で。

assertThat() メソッドによるアサーション

次は assertThat() メソッドに関連するアサーションを見ていきます。 このメソッドは JUnit4.4 あたりで導入されたそうなので、それ以降のバージョンを使ってください(現在の最新は JUnit4.10 なのでまぁ大丈夫だと思いますが)。

このメソッドは2つの引数をとり、第1引数のオブジェクトが主語(Object なのに Subject (笑))、第2引数の org.hamcrest.Matcher<T> オブジェクトが述語の役割をします。 アサーションは「主語が述語を満たす」なら通ります。 例えば

String s0 = ...
String s1 = ...
assertThat( s0, is(not(equalTo(s1))) );

となっているとき、第1引数(s0)が主語で、第2引数(「is(not(equalTo(s1)))」 の返り値である Matcher オブジェクト)が述語になります。 is, not, equalTo などは述語を生成するメソッドで、上記のコードでは省略してますが通常は static import によってクラス参照なしに使えるようにしておきます。 assertThat の部分が、

assert that s0 is not equal to s1 // 「s0 は s1 と等しくない」ことを確証する

のように(括弧などを無視すれば)英文のように書けるよーって話です。 日本人にとって、英文のように書けることがどの程度のメリットなのかはよくわかりませんが・・・

まぁそれはともかく、このアサーション方法では assertXxxx() メソッドをあれこれ覚える代わりに、述語を生成する方法(というか、まぁ使える述語)を把握しておく必要があります。 述語は、先ほども書いたとおり org.hamcrest.Matcher オブジェクトで表されますが、Matcher オブジェクトはコンストラクタから直接インスタンス化させるわけではなく、種々の(ファクトリ)クラスの static ファクトリ・メソッドから生成します。 これらのクラスは主に

  • org.hamcrest.core パッケージの各クラス
  • org.junit.matcher.JUnitMatcher クラス

の2種類があります。 以下でそれぞれから生成できる述語を見ていきましょう。

org.hamcrest.core パッケージに定義されている述語
まずは org.hamcrest.core パッケージに定義されている(ファクトリ)クラスから。 結構たくさんクラスがあります。 ファクトリ・メソッドはほとんどジェネリックなメソッドで、返り値は Matcher<T> です(is(Class), instanceOf(Class) メソッドの返り値は例外的に Matcher<Object>)。

追記

下記の述語は org.hamcrest.CoreMatchers クラスの static メソッドとしても定義されているので、複数使う場合は CoreMatchers の static メソッドを static import した方が楽そうです。

import static org.hamcrest.CoreMatchers.*;

JUnit の JavaDoc には CoreMatchers のドキュメントが載ってないので公式の方法なのかどうかは分かりませんが・・・

ファクトリ・クラス ファクトリ・メソッド テストが通る条件
Is is(Matcher)
is(T)
is(Class)
主語が引数のテストを通る
= is(equalTo(T))
= is(instanceOf(Class))
IsAnything anything()
anything(String)
any(Class<T>)
常にテストが通る
IsNot not(Matcher)
not(T)
主語が引数のテストを通らない
= not(equalTo(T))
IsNull nullValue()
nullValue(Class)
notNullValue()
notNullValue(Class)
主語が null

主語が null でない
IsSame sameInstance(T) 主語が引数のオブジェクトと同じオブジェクト
IsInstanceOf instanceOf(Class) 主語が引数のクラスのインスタンスである
IsEqual equalTo(T) 主語が引数のオブジェクトと
Object#equals() で等しい
AllOf allOf(Iterable>)
allOf(Matcher<? extends T>...)
主語が引数の全てのテストを通る
AnyOf anyOf(Iterable>)
anyOf(Matcher<? extends T>..)
主語が引数のいずれかのテストを通る

使い方は下記のサンプル・コードにて。

JUnitMatchers クラスに定義されている述語
次は org.junit.matcher.JUnitMatchers クラスに定義されている static メソッド。 返り値は Matcher<T> もしくはそのサブタイプです。

メソッド テストが通る条件
containsString(String) 引数の文字列を部分文字列として含む
both(Matcher)
either(Matcher)
引数のテストと and() で生成されたテストが通る
引数のテストもしくは or() で生成されたテストが通る
everyItem(Matcher) 主語のコレクションの各要素が全てテストを通る
hasItem(Matcher)
hasItem(T)
主語のコレクションの要素のうち少なくとも1つがテストを通る
hasItems(Matcher...)
hasItems(T...)
引数の各テストに対して、主語のコレクションの要素のうち少なくとも1つがテストを通る

これらもあれこれ説明するよりサンプル・コード見た方が早いかと。 JUnitMatchers に定義されている述語はサンプル・コードの「4」に書いてます。

サンプル・コード

では各アサーションメソッドを使ったサンプルを見ていきましょう。 一応4つくらいに分けてみました:

  1. null 値関連のテスト・型チェックテスト
  2. 同値関係のテスト
  3. 複数の述語を組み合わせるテスト
  4. JUnitMatchers クラスに定義されている述語のテスト

1. 〜 3. では、同じようなテストを以下のような方法で複数書いてます:

  • assert キーワードで書いたテスト
  • Assert の static メソッド(assertThat() を除く)で書いたテスト
  • Assert#assertThat() で書いたテスト

assertThat() メソッドを使ったテストはさらに複数書いてるモノもあります。 そのせいで結構助長になってたりしますがご勘弁を。

1. null 値関連のテスト・型チェックテスト
null 値かどうかのテストと instanceof に対応する型チェックテスト:

import org.junit.Test;

import static org.junit.Assert.*;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsAnything.any;
import static org.hamcrest.core.IsAnything.anything;
import static org.hamcrest.core.IsInstanceOf.instanceOf;
import static org.hamcrest.core.IsNull.*;
//import static org.hamcrest.CoreMatchers.*;

public class SampleTest1 {

    @Test public void null_object(){
        String s = null;

        // assert キーワード
        assert s == null;

        // Assert
        assertNull( s );

        // Assert.assertThat()
        assertThat( s, is(nullValue()) );
        assertThat( s, nullValue() );
        assertThat( s, nullValue(String.class) );    // type inference
    }

    @Test public void not_null_object(){
        String s = "abc";

        // assert キーワード
        assert s != null;

        // Assert
        assertNotNull( s );

        // Assert.assertThat()
        assertThat( s, is(notNullValue()) );
        assertThat( s, notNullValue() );
        assertThat( s, notNullValue(String.class) );    // type inference
    }

    @Test public void type_check(){
        Object obj = "abc";

        // assert キーワード
        assert obj instanceof String;

        // Assert
        assertTrue( obj instanceof String );

        // Assert.assertThat()
        assertThat( obj, is(instanceOf(String.class)) );
        assertThat( obj, instanceOf(String.class) );
        assertThat( obj, is(String.class) );
    }

    @Test public void check_anything(){
        String s = null;

        // assert キーワード
        assert true;

        // Assert.assertThat()
        assertThat( s, is(anything()) );
        assertThat( s, anything() );
        assertThat( s, any(String.class) );    // type inference
        assertThat( s, any(Object.class) );    // type inference
    }
}
  • 「type inference」は型推論。 ジェネリックなメソッドの型パラメータを明示的に指定していることを示しています。
  • assertThat() メソッドを使ってる箇所では、英文っぽくなるようにやたらと「is」を使ってるのもありますが、必要ない場合も多いようです。

2. 同値関係のテスト
Java 関連のプログラミングで常に気にすべき、「==」によるテスト(評価)と「Object#equals()」によるテスト(評価):

import org.junit.Test;
import static org.junit.Assert.*;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.hamcrest.core.IsNot.not;
import static org.hamcrest.core.IsSame.sameInstance;
//import static org.hamcrest.CoreMatchers.*;

public class SampleTest2 {

    @Test public void same_objects(){
        String s0 = "abc";
        String s1 = s0;

        //***** == でのテスト (true) *****
        assert s0 == s1;

        assertSame( s0, s1 );

        assertThat( s0, is(sameInstance(s1)) );

        //***** Object#equals() でのテスト (true) *****
        assert s0.equals( s1 );

        assertEquals( s0, s1);

        assertThat( s0, is(equalTo(s1)) );
        assertThat( s0, equalTo(s1) );
        assertThat( s0, is(s1) );
    }

    @Test public void equivalent_but_not_same_objects(){
        String s0 = "a" + "b";
        String s1 = "abc".substring(0, 2);

        //***** == でのテスト (false) *****
        assert s0 != s1;

        assertNotSame( s0, s1 );

        assertThat( s0, is(not(sameInstance(s1))) );
        assertThat( s0, not(sameInstance(s1)) );

        //***** Object#equals() でのテスト (true) *****
        assert s0.equals( s1 );

        assertEquals( s0, s1 );

        assertThat( s0, is(equalTo(s1)) );
        assertThat( s0, equalTo(s1) );
        assertThat( s0, is(s1) );
    }

    @Test public void not_equivalent_objects(){
        String s0 = "abc";
        String s1 = "xyz";

        //***** == でのテスト (false) *****
        assert s0 != s1;

        assertNotSame( s0, s1 );

        assertThat( s0, is(not(sameInstance(s1))) );
        assertThat( s0, not(sameInstance(s1)) );

        //***** Object#equals() でのテスト (false) *****
        assert !s0.equals(s1);

        assertThat( s0, is(not(equalTo(s1))) );
        assertThat( s0, not(equalTo(s1)) );
        assertThat( s0, is(not(s1)) );
        assertThat( s0, not(s1) );
    }
}
  • assertEquals() では引数の順序を逆にすべきな気がしますが、書き換えるのが面倒なのでそのままに。 テストが失敗したときにメッセージがおかしくなるかも。

3. 複数の述語を組み合わせるテスト
複数の述語は allOf() と anyOf() で組み合わせることが出来ます。 それぞれ && と || みたいなもの。

import org.junit.Test;
import static org.junit.Assert.assertThat;
import static org.hamcrest.core.AllOf.allOf;
import static org.hamcrest.core.AnyOf.anyOf;
import static org.hamcrest.core.IsNull.*;
import static org.hamcrest.core.IsInstanceOf.instanceOf;
import static org.hamcrest.core.IsSame.sameInstance;
import static org.hamcrest.core.IsNot.not;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.hamcrest.core.Is.is;
//import static org.hamcrest.CoreMatchers.*;

public class SampleTest3 {

    @Test @SuppressWarnings("unchecked")
    public void pass_all_conditions(){
        Object str = "abc";
        String abc = "abcd".substring(0, 3);

        // assert キーワード
        assert (str != null) && (str instanceof String) && (str != abc) && str.equals(abc);

        // Assert.assertThat
        assertThat( str, allOf( notNullValue(), instanceOf(String.class), not(sameInstance(abc)), equalTo(abc) ) );
    }

    @Test @SuppressWarnings("unchecked")
    public void pass_any_condition(){
        String str = "abc";

        // assert キーワード
        assert (str == null) || str.equals("abc") || str.equals("xyz");

        // Assert.assertThat
        assertThat( str, anyOf( nullValue(), is("abc"), is("xyz") ) );
    }
}
  • なんか、無理に assertThat() メソッド使っても読みにくくなるだけかも・・・ assertThat() を使うと主語が明確になるので、主語の参照「str」を一箇所に書くだけでいい、というのが唯一のメリットな気がするけど(英語が母国語でないなら)、これはこれで融通が利かなくなってるよなぁ。 例えば文字列の長さをテストしたい場合は、別途アサーションを書かないといけない・・・

4. JUnitMatchers クラスに定義されている述語のテスト
最後は JUnitMatchers クラスで使える述語:

import org.junit.Test;

import java.util.Arrays;
import java.util.List;

import static org.junit.Assert.assertThat;
import static org.junit.matchers.JUnitMatchers.*;
import static org.hamcrest.core.IsInstanceOf.instanceOf;
import static org.hamcrest.core.IsNull.notNullValue;
import static org.hamcrest.core.IsNull.nullValue;
//import static org.hamcrest.CoreMatchers.*;

public class SampleTest4 {

    @Test public void test_both_and(){
        String s = "abcdefghijklmnopqrstuvwxyz";

        assertThat( s, both (notNullValue()) .and (instanceOf(String.class)) .and(containsString("abc")) );
    }

    @Test public void test_either_or(){
        String s = "abcdefghijklmnopqrstuvwxyz";

        assertThat( s, either (nullValue()) .or (instanceOf(Integer.class)) .or(containsString("stuv")) );
    }

    @Test public void test_everyItem(){
        List<String> list = Arrays.asList("java", "groovy", "velocity", "ivy");

        assertThat( list, everyItem(containsString("v")) );
    }

    @Test public void test_hasItem(){
        List<String> list = Arrays.asList("java", "groovy", "velocity", "ivy");

        assertThat( list, hasItem("groovy") );
        assertThat( list, hasItem(containsString("j")) );
    }

    @Test @SuppressWarnings("unchecked")
    public void test_hasItems(){
        List<String> list = Arrays.asList("java", "groovy", "velocity", "ivy");

        assertThat( list, hasItems("java", "groovy") );
        assertThat( list, hasItems(containsString("j"), containsString("o")) );
    }
}
  • both, either は引数には1つの述語を書き、both/either で返されるオブジェクトに対して and/or メソッドを呼び出して別の述語を渡す、という風に使います。 both で返されるオブジェクトに or を呼び出したりもできるようですが、それをしても得をする人間はいないでしょう。
  • hasItems() では、テストを通る(主語のコレクションの)要素は各テストで異なっていても良いようです。

実践テスト駆動開発 テストに導かれてオブジェクト指向ソフトウェアを育てる (Object Oriented SELECTION)

実践テスト駆動開発 テストに導かれてオブジェクト指向ソフトウェアを育てる (Object Oriented SELECTION)

テスト駆動開発入門

テスト駆動開発入門

  • 作者: ケントベック,Kent Beck,長瀬嘉秀,テクノロジックアート
  • 出版社/メーカー: ピアソンエデュケーション
  • 発売日: 2003/09
  • メディア: 単行本
  • 購入: 45人 クリック: 1,058回
  • この商品を含むブログ (161件) を見る