AssertJ 使い方メモ

  • 32
    いいね
  • 4
    コメント

AssertJ とは

JUnit で使えるアサーション(検証)用のライブラリ(フレームワーク?)。

assertTrue とか、 assertThat とか、ああいうの。

JUnit4 の標準は Hamcrest だが、 AssertJ を使った検証に差し替えることもできる。

AssertJ も Hamcrest と同様で流れるようなインターフェースで検証が書ける。
しかし、 AssertJ はメソッドチェーンができるように API が定義されているので、 Hamcrest よりも IDE の補完を活用してサクサクと検証を書くことができる。

2015年12月現在、 ver 2.x 系と 3.x 系が開発されていて、 3.x 系は Java 8 に対応している(Optional とか Date Time API の検証とかに対応しているっぽい)。
今回は、 3.x 系の使い方を調べる。

Hello World

インストール

build.gradle
    testCompile 'org.assertj:assertj-core:3.2.0'

実装

MainTest.java
package sample.assertj;

import org.junit.Test;
import static org.assertj.core.api.Assertions.*;

public class MainTest {

    @Test
    public void test() {
        assertThat("hoge").isEqualTo("Hoge");
    }
}

実行

実行結果
org.junit.ComparisonFailure: expected:<"[H]oge"> but was:<"[h]oge">
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at sample.assertj.MainTest.test(MainTest.java:10)
(以下略)

説明

  • org.assertj.core.api.Assertions クラスに static で定義されている assertThat() メソッドを起点にして検証を書く。
  • assertThat() の引数に、検証したい対象の値を渡す。
  • 続けてメソッドチェーンを利用して期待する値を宣言する(isEqualTo())。
  • assertThat() は渡される引数の型に応じてオーバロードされている。
    • 引数の型に応じて専用の検証用オブジェクトが返されるので、型ごとに連鎖できるメソッドが限定されるようになる。
    • Eclipse などの IDE を使っていれば入力補完で一覧が表示されるので、以下のようなメリットがある。
      • Collection にしか使えない検証などを、間違って String 相手に使うようなミスを防げる。
      • 入力候補が一覧表示されるので、どんな検証が使えるのか俯瞰できる。

IDE (Eclipse) の入力補完設定

検証には Assertions クラスの static メソッドを利用する。
なので、 Assertions クラスの static メソッドは簡単に静的インポートできるようにしておくと捗る。

Eclipse の場合は、「インポートの編成」と「お気に入り」を変更することで対応できる。

「インポートの編成」を変更する

  1. [ウィンドウ] → [設定]
  2. [Java] → [コード・スタイル] → [インポートの編成]
  3. 「.* に必要な静的インポート数」の設定値を 1 にする。

assertj.JPG

「お気に入り」を変更する

  1. [ウィンドウ] → [設定]
  2. [Java] → [エディター] → [コンテンツ・アシスト] → [お気に入り]
  3. [新規タイプ] ボタンをクリック
  4. 「完全修飾型を入力」に org.assertj.core.api.Assertions と入力して [OK]。

assertj.JPG

すでに JUnit や Hamcrest 用の設定がある場合は、 assertThat が競合する。
完全に AssertJ に移行するなら、外しておいたほうがいいかもしれない。

これで assertThat とエディタ上で入力すれば、 Assertions のメソッドが補完候補として表示され、入力を確定させれば勝手に Assertions のメンバーを静的インポートしてくれるようになり捗る。

他の IDE (NetBeans, IntelliJ Idea など)も同じような設定があるはず。たぶん。

検証に説明を追加する

MainTest.java
package sample.assertj;

import org.junit.Test;
import static org.assertj.core.api.Assertions.*;

public class MainTest {

    @Test
    public void test() {
        assertThat("hoge")
            .as("説明だよ!")
            .isEqualTo("Hoge");
    }
}
実行結果
org.junit.ComparisonFailure: [説明だよ!] expected:<"[H]oge"> but was:<"[h]oge">
(以下略)
  • as() メソッドで、検証に説明を追加できる。
  • 要注意なのが、 as() メソッドは検証メソッドの前に呼び出しておく必要がある 点。
    • 後ろに書くと表示されない!
    • assertThat() の直後に書くようにしておくといい。

途中で検証が失敗しても残りの検証を続行させる

MainTest.java
package sample.assertj;

import org.assertj.core.api.SoftAssertions;
import org.junit.Test;

public class MainTest {

    @Test
    public void test() {
        SoftAssertions softly = new SoftAssertions();

        softly.assertThat(10).isEqualTo(9);
        softly.assertThat("hoge").isEqualTo("hoge");
        softly.assertThat(false).isEqualTo(true);

        softly.assertAll();
    }
}
実行結果
org.assertj.core.api.SoftAssertionError: 
The following 2 assertions failed:
1) expected:<[9]> but was:<[10]>
2) expected:<[tru]e> but was:<[fals]e>
(以下略)
  • SoftAssertions を使うと、途中で検証が失敗しても残りの検証を続行できる。
  • 最後に assertAll() メソッドを呼ぶと、全ての検証が実行される。
  • 失敗した検証のメッセージが全て出力される。
  • あまり1つのテストケースに複数の検証を書くのは良くないので、多用は禁物かも。

JUnit の @Rule を使う

MainTest.java
package sample.assertj;

import org.assertj.core.api.JUnitSoftAssertions;
import org.junit.Rule;
import org.junit.Test;

public class MainTest {

    @Rule
    public JUnitSoftAssertions softly = new JUnitSoftAssertions();

    @Test
    public void test() {
        softly.assertThat(10).isEqualTo(9);
        softly.assertThat("hoge").isEqualTo("hoge");
        softly.assertThat(false).isEqualTo(true);
    }
}
実行結果
org.junit.ComparisonFailure: expected:<[9]> but was:<[10]>
(以下略)

org.junit.ComparisonFailure: expected:<[tru]e> but was:<[fals]e>
(以下略)
  • JUnit を使っている場合は、 @Rule を使う方法も用意されている。

いろいろな検証メソッド

たいていのメソッドは、名前を見れば何を検証するのかが想像できる(スバラシイ!!)ので、ここでは気になったやつだけピックアップ。

数値

範囲の検証 - isBetween()

MainTest.java
package sample.assertj;

import org.assertj.core.api.SoftAssertions;
import org.junit.Test;

public class MainTest {

    @Test
    public void test() throws Exception {
        SoftAssertions softly = new SoftAssertions();

        softly.assertThat(5).as("NG 1").isBetween(3, 4);
        softly.assertThat(5).as("NG 2").isBetween(6, 7);
        softly.assertThat(5).as("OK 3").isBetween(4, 5);
        softly.assertThat(5).as("OK 4").isBetween(5, 6);

        softly.assertAll();
    }
}
実行結果
org.assertj.core.api.SoftAssertionError: 
The following 2 assertions failed:
1) [NG 1] 
Expecting:
 <5>
to be between:
 [3, 4]
2) [NG 2] 
Expecting:
 <5>
to be between:
 [6, 7]

...
  • 開始・終了を指定して、その範囲内に入っているか検証する。
  • 引数の値を 含む 範囲で検証する。

範囲の検証 - isCloseTo()

MainTest.java
package sample.assertj;

import static org.assertj.core.api.Assertions.*;

import org.assertj.core.api.SoftAssertions;
import org.junit.Test;

public class MainTest {

    @Test
    public void test() throws Exception {
        SoftAssertions softly = new SoftAssertions();

        softly.assertThat(5).as("NG 1").isCloseTo(7, within(1));
        softly.assertThat(5).as("NG 2").isCloseTo(3, within(1));
        softly.assertThat(5).as("OK 3").isCloseTo(7, within(2));
        softly.assertThat(5).as("OK 4").isCloseTo(3, within(2));

        softly.assertAll();
    }
}
実行結果
org.assertj.core.api.SoftAssertionError: 
The following 2 assertions failed:
1) [NG 1] 
Expecting:
  <5>
to be close to:
  <7>
by less than <1> but difference was <2>.
(a difference of exactly <1> being considered valid)
2) [NG 2] 
Expecting:
  <5>
to be close to:
  <3>
by less than <1> but difference was <2>.
(a difference of exactly <1> being considered valid)
...
  • 起点と前後のオフセットを指定して、その範囲内に収まるかどうかを検証する。
  • こちらも、 含む 範囲での検証になる。

String

含まれる文字の検証 - contains()

MainTest.java
package sample.assertj;

import org.assertj.core.api.SoftAssertions;
import org.junit.Test;

public class MainTest {

    @Test
    public void test() throws Exception {
        SoftAssertions softly = new SoftAssertions();

        softly.assertThat("hoge").as("NG 1").contains("h", "g", "A");
        softly.assertThat("hoge").as("OK 2").contains("h", "g", "e");

        softly.assertAll();
    }

}
実行結果
org.assertj.core.api.SoftAssertionError: 
The following assertion failed:
1) [NG 1] 
Expecting:
 <"hoge">
to contain:
 <["h", "g", "A"]>
but could not find:
 <["A"]>
...
  • 全ての文字を含むことを検証する。

順序通りに含まれることを検証 - containsSequence()

MainTest.java
package sample.assertj;

import org.assertj.core.api.SoftAssertions;
import org.junit.Test;

public class MainTest {

    @Test
    public void test() throws Exception {
        SoftAssertions softly = new SoftAssertions();

        softly.assertThat("hoge").as("NG 1").containsSequence("g", "o", "e");
        softly.assertThat("hoge").as("OK 2").containsSequence("h", "o", "e");

        softly.assertAll();
    }

}
実行結果
org.assertj.core.api.SoftAssertionError: 
The following assertion failed:
1) [NG 1] 
Expecting:
 <"hoge">
to contain the following CharSequences in this order:
 <["g", "o", "e"]>
but <"o"> was found before <"g">
...
  • 指定した文字列が、指定した順序で現れることを検証する。

行数を検証する - hasLineCount()

MainTest.java
package sample.assertj;

import org.assertj.core.api.SoftAssertions;
import org.junit.Test;

public class MainTest {

    @Test
    public void test() throws Exception {
        SoftAssertions softly = new SoftAssertions();

        softly.assertThat("hoge").as("NG 1").hasLineCount(2);
        softly.assertThat("hoge\nfuga").as("OK 2").hasLineCount(2);
        softly.assertThat("hoge\rfuga").as("OK 3").hasLineCount(2);
        softly.assertThat("hoge\r\nfuga").as("OK 4").hasLineCount(2);

        softly.assertAll();
    }

}
実行結果
org.assertj.core.api.SoftAssertionError: 
The following assertion failed:
1) [NG 1] 
Expecting text:
"hoge"
to have <2> lines but had <1>.
...
  • \r または \n または \r\n のいずれかがあると改行として判定されるもよう。
  • つまり、 OS の違いを気にせず検証できる。

空文字の検証 - isEmpty()

MainTest.java
package sample.assertj;

import org.assertj.core.api.SoftAssertions;
import org.junit.Test;

public class MainTest {

    @Test
    public void test() throws Exception {
        SoftAssertions softly = new SoftAssertions();

        String nullValue = null;

        softly.assertThat("hoge").as("NG 1").isEmpty();
        softly.assertThat(nullValue).as("NG 2").isEmpty();
        softly.assertThat("").as("OK 3").isEmpty();

        softly.assertAll();
    }

}
実行結果
org.assertj.core.api.SoftAssertionError: 
The following 2 assertions failed:
1) [NG 1] 
Expecting empty but was:<"hoge">
2) [NG 2] 
Expecting actual not to be null
...
  • 空文字かどうかを検証する。
  • null は検証エラーになる。
  • null も許容する場合は、 isNullOrEmpty() を使う。

正規表現での検証 - matches()

MainTest.java
package sample.assertj;

import org.assertj.core.api.SoftAssertions;
import org.junit.Test;

public class MainTest {

    @Test
    public void test() throws Exception {
        SoftAssertions softly = new SoftAssertions();

        softly.assertThat("hoge").as("NG 1").matches("h");
        softly.assertThat("hoge").as("NG 2").matches("[a-z]{2}");
        softly.assertThat("hoge").as("OK 3").matches("h...");
        softly.assertThat("hoge").as("OK 4").matches(".*e$");

        softly.assertAll();
    }
}
実行結果
org.assertj.core.api.SoftAssertionError: 
The following 2 assertions failed:
1) [NG 1] 
Expecting:
 "hoge"
to match pattern:
 "h"
2) [NG 2] 
Expecting:
 "hoge"
to match pattern:
 "[a-z]{2}"
...
  • 正規表現で指定したパターンに、完全に一致するかどうかを検証する。
  • 一部が一致していてもダメ。

Iterable

要素が含まれることの検証 - contains()

MainTest.java
package sample.assertj;

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

import org.assertj.core.api.SoftAssertions;
import org.junit.Test;

public class MainTest {

    @Test
    public void test() throws Exception {
        SoftAssertions softly = new SoftAssertions();

        List<String> list = Arrays.asList("hoge", "fuga", "piyo");

        softly.assertThat(list).as("OK 1").contains("fuga", "piyo");
        softly.assertThat(list).as("NG 1").contains("hoge", "fizz");

        softly.assertAll();
    }
}
実行結果
org.assertj.core.api.SoftAssertionError: 
The following assertion failed:
1) [NG 1] 
Expecting:
 <["hoge", "fuga", "piyo"]>
to contain:
 <["hoge", "fizz"]>
but could not find:
 <["fizz"]>
...
  • 要素の順序は検証しない。
  • 全部含まれるかどうかも検証しない。
  • 期待値を List などで渡したい場合は containsAll() を使用する。

指定した要素だけが含まれることを検証する - containsOnly()

MainTest.java
package sample.assertj;

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

import org.assertj.core.api.SoftAssertions;
import org.junit.Test;

public class MainTest {

    @Test
    public void test() throws Exception {
        SoftAssertions softly = new SoftAssertions();

        List<String> list = Arrays.asList("hoge", "fuga", "piyo");

        softly.assertThat(list).as("OK 1").containsOnly("hoge", "fuga", "piyo");
        softly.assertThat(list).as("OK 2").containsOnly("piyo", "fuga", "hoge");
        softly.assertThat(list).as("NG 3").containsOnly("hoge", "fuga");
        softly.assertThat(list).as("NG 4").containsOnly("hoge", "fuga", "piyo", "fizz");

        softly.assertAll();
    }
}
実行結果
org.assertj.core.api.SoftAssertionError: 
The following 2 assertions failed:
1) [NG 3] 
Expecting:
  <["hoge", "fuga", "piyo"]>
to contain only:
  <["hoge", "fuga"]>
but the following elements were unexpected:
  <["piyo"]>

2) [NG 4] 
Expecting:
  <["hoge", "fuga", "piyo"]>
to contain only:
  <["hoge", "fuga", "piyo", "fizz"]>
but could not find the following elements:
  <["fizz"]>
...
  • 指定した要素以外が含まれる場合は検証エラー。
  • 指定した要素が含まれない場合も検証エラー。
  • 順序は問わない。
  • 期待値に List などを渡したい場合は containsOnlyElementsOf() を使う。

要素が順序も含め完全に一致することを検証する - containsExactly()

MainTest.java
package sample.assertj;

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

import org.assertj.core.api.SoftAssertions;
import org.junit.Test;

public class MainTest {

    @Test
    public void test() throws Exception {
        SoftAssertions softly = new SoftAssertions();

        List<String> list = Arrays.asList("hoge", "fuga", "piyo");

        softly.assertThat(list).as("OK 1").containsExactly("hoge", "fuga", "piyo");
        softly.assertThat(list).as("NG 2").containsExactly("piyo", "fuga", "hoge");
        softly.assertThat(list).as("NG 3").containsExactly("hoge", "fuga");
        softly.assertThat(list).as("NG 4").containsExactly("hoge", "fuga", "piyo", "fizz");

        softly.assertAll();
    }
}
実行結果
org.assertj.core.api.SoftAssertionError: 
The following 3 assertions failed:
1) [NG 2] 
Actual and expected have the same elements but not in the same order, at index 0 actual element was:
  <"hoge">
whereas expected element was:
  <"piyo">

2) [NG 3] 
Actual and expected should have same size but actual size was:
  <3>
while expected size was:
  <2>
Actual was:
  <["hoge", "fuga", "piyo"]>
Expected was:
  <["hoge", "fuga"]>

3) [NG 4] 
Actual and expected should have same size but actual size was:
  <3>
while expected size was:
  <4>
Actual was:
  <["hoge", "fuga", "piyo"]>
Expected was:
  <["hoge", "fuga", "piyo", "fizz"]>
...
  • 要素が順序含め完全に一致することを検証できる。
  • 期待値に List などを渡したい場合は、 containsExactlyElementsOf() を使う。

要素の一部が指定した順序で存在することを検証する - containsSequence()

MainTest.java
package sample.assertj;

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

import org.assertj.core.api.SoftAssertions;
import org.junit.Test;

public class MainTest {

    @Test
    public void test() throws Exception {
        SoftAssertions softly = new SoftAssertions();

        List<String> list = Arrays.asList("hoge", "fuga", "piyo");

        softly.assertThat(list).as("OK 1").containsSequence("hoge", "fuga", "piyo");
        softly.assertThat(list).as("OK 2").containsSequence("fuga", "piyo");
        softly.assertThat(list).as("NG 3").containsSequence("fuga", "hoge");
        softly.assertThat(list).as("NG 4").containsSequence("hoge", "piyo");

        softly.assertAll();
    }
}
実行結果
org.assertj.core.api.SoftAssertionError: 
The following 2 assertions failed:
1) [NG 3] 
Expecting:
 <["hoge", "fuga", "piyo"]>
to contain sequence:
 <["fuga", "hoge"]>

2) [NG 4] 
Expecting:
 <["hoge", "fuga", "piyo"]>
to contain sequence:
 <["hoge", "piyo"]>
...
  • 登場する順序は正しくても、間が飛んでいると検証エラーになる(NG 4)。
    • 間を飛ばしても検証 OK にしたい場合は containsSubsequence() を使う。
  • 期待値に List などを渡したい場合は...ない?

条件を満たす要素だけを検証する - filteredOn()

MainTest.java
package sample.assertj;

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

import org.assertj.core.api.SoftAssertions;
import org.junit.Test;

public class MainTest {

    @Test
    public void test() throws Exception {
        SoftAssertions soflty = new SoftAssertions();

        List<String> list = Arrays.asList("one", "two", "three", "four");

        soflty.assertThat(list)
            .as("OK 1")
            .filteredOn(str -> str.contains("o"))
            .containsExactly("one", "two", "four");

        soflty.assertThat(list)
            .as("OK 2")
            .filteredOn(str -> str.contains("t"))
            .containsExactly("two", "three");
    }
}
  • filteredOn() で、条件を満たした要素だけが次の検証に進む。
  • Condition を受け取ることもできるので、 Java8 より前はそちらを使うのも手。

各要素が持つ特定の属性だけを抽出して検証する - extracting()

MainTest.java
package sample.assertj;

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

import org.assertj.core.api.SoftAssertions;
import org.junit.Test;

public class MainTest {

    @Test
    public void test() throws Exception {
        SoftAssertions soflty = new SoftAssertions();

        List<Person> people = Arrays.asList(
                new Person("Sato", 15),
                new Person("Suzuki", 18),
                new Person("Tanaka", 17)
            );

        soflty.assertThat(people)
            .as("OK 1")
            .extracting(person -> person.getName())
            .containsExactly("Sato", "Suzuki", "Tanaka");

        soflty.assertThat(people)
            .as("OK 2")
            .extracting(person -> person.getAge())
            .containsExactly(15, 18, 17);
    }

    private static class Person {
        private String name;
        private int age;

        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public String getName() {
            return name;
        }

        public int getAge() {
            return age;
        }
    }
}
  • extracting()Function を渡すことで、各要素の値を差し替えたコレクションに対して検証ができる。
  • Stream API の map() と同じ感じ。
  • Java8 ならラムダがあるので、これだけあれば事足りる。
  • Java8 以前の場合は、プロパティ名を指定して抽出する extracting(String propertyName) とか、メソッドを実行してその結果を得る extracting(String methodName) が用意されている。

フラットに抽出する - flatExtracting()

MainTest.java
package sample.assertj;

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

import org.assertj.core.api.SoftAssertions;
import org.junit.Test;

public class MainTest {

    @Test
    public void test() throws Exception {
        SoftAssertions soflty = new SoftAssertions();

        List<Person> people = Arrays.asList(
                new Person("running", "cooking"),
                new Person("swimming"),
                new Person("programming", "reading books")
            );

        soflty.assertThat(people)
            .as("OK")
            .flatExtracting(person -> person.getHobbies())
            .containsExactly("running", "cooking", "swimming", "progmramming", "reading books");
    }

    private static class Person {
        private List<String> hobbies;

        public Person(String... hobbies) {
            this.hobbies = Arrays.asList(hobbies);
        }

        public List<String> getHobbies() {
            return hobbies;
        }
    }
}
  • flatExtracting() で、抽出したコレクションをフラットな状態にして検証できる。
  • Stream API の flatMap() と同じ感じ。

Date

文字列の日付と比較 - hasSameTimeAs()

MainTest.java
package sample.assertj;

import static org.assertj.core.api.Assertions.*;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

import org.junit.Test;

public class MainTest {

    private Date date = date("2015-12-23 15:16:17");

    @Test
    public void test1() throws Exception {
        assertThat(date).as("OK 1").hasSameTimeAs("2015-12-23T15:16:17");
    }

    @Test
    public void test2() throws Exception {
        assertThat(date).as("NG 2").hasSameTimeAs("2015-12-23T15:16:18");
    }

    private Date date(String text) {
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        try {
            return format.parse(text);
        } catch (ParseException e) {
            throw new RuntimeException(e);
        }
    }
}
実行結果
java.lang.AssertionError: [NG 2] 
Expecting
  <2015-12-23T15:16:17.000>
to have the same time as:
  <2015-12-23T15:16:18.000>
but actual time is
  <1450851377000L>
and expected was:
  <1450851378000L>
...
  • hasSameTimeAs() に日付文字列を渡すことで検証ができる。
  • 対応しているフォーマットは以下。
    • yyyy-MM-dd'T'HH:mm:ss.SSS
    • yyyy-MM-dd HH:mm:ss.SSS
    • yyyy-MM-dd'T'HH:mm:ss
    • yyyy-MM-dd
  • SoftAssertions で使おうとすると、 ClassCastException がスローされた。。。バグかな。。。

任意の日付書式を追加する - registerCustomDateFormat()

MainTest.java
package sample.assertj;

import static org.assertj.core.api.Assertions.*;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

import org.assertj.core.api.AbstractDateAssert;
import org.junit.Test;

public class MainTest {

    private Date date = date("2015-12-23 15:16:17");

    @Test
    public void test1() throws Exception {
        AbstractDateAssert.registerCustomDateFormat("yyyy/MM/dd HH:mm:ss");
        assertThat(date).as("OK 1").hasSameTimeAs("2015/12/23 15:16:17");
    }

    @Test
    public void test2() throws Exception {
        assertThat(date).as("OK 2").hasSameTimeAs("2015/12/23 15:16:17");
    }

    private Date date(String text) {
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        try {
            return format.parse(text);
        } catch (ParseException e) {
            throw new RuntimeException(e);
        }
    }
}
  • 任意の日付書式を追加登録できる。
  • 既存の書式も、そのまま利用できる。
  • 一度登録すると、その書式はテスト内で共通で使われるようになる。

File

中身を文字列で比較する - hasContent()

text.txt
abcdefg
hijklmn
MainTest.java
package sample.assertj;

import static org.assertj.core.api.Assertions.*;

import java.io.File;

import org.junit.Test;

public class MainTest {

    @Test
    public void test() throws Exception {
        assertThat(new File("F:/tmp/text.txt")).hasContent("abcdefg\nhijklmnd");
    }
}
実行結果
java.lang.AssertionError: 
File:
  <F:\tmp\text.txt>
read with charset <UTF-8> does not have the expected content:
line:<2>, expected:<hijklmnd> but was:<hijklmn>
...
  • 行数も見てくれる。

文字コードを指定する - usingCharset()

text_sjis.txt
あいうえお
かきくけこ
MainTest.java
package sample.assertj;

import static org.assertj.core.api.Assertions.*;

import java.io.File;

import org.junit.Test;

public class MainTest {

    @Test
    public void test() throws Exception {
        assertThat(new File("F:/tmp/text_sjis.txt"))
            .usingCharset("Shift_JIS")
            .hasContent("あいうえお\r\nかきくけこさ");
    }
}
実行結果
java.lang.AssertionError: 
File:
  <F:\tmp\text_sjis.txt>
read with charset <Shift_JIS> does not have the expected content:
line:<2>, expected:<かきくけこさ> but was:<かきくけこ>
...
  • usingCharset() で文字コードを指定できる。
  • hasContent() の前に実行しないといけない。

Condition

基本

MainTest.java
package sample.assertj;

import static org.assertj.core.api.Assertions.*;

import org.assertj.core.api.Condition;
import org.junit.Test;

public class MainTest {

    private MyCondition myCondition = new MyCondition();

    @Test
    public void test() throws Exception {
        assertThat("one").is(myCondition);
    }

    private static class MyCondition extends Condition<String> {

        private String value;

        @Override
        public boolean matches(String value) {
            this.value = value;
            return value.length() == 4;
        }

        @Override
        public String toString() {
            return "\"" + this.value + "\" は4桁じゃない!";
        }
    }
}
実行結果
java.lang.AssertionError: 
Expecting:
 <"one">
to be <"one" は4桁じゃない!>
(以下略)
  • Condition クラスを継承したクラスを作成する。
  • matches() メソッドをオーバーライドして、検証処理を実装する。
    • 結果は boolean で返す(検証OKなら true)。
  • toString() メソッドの戻り値が、エラーメッセージの中で利用されるので分かりやすい説明を返すようにしておいたほうがよさげ。
  • 自作の Condition は、 is() などの Condition を受け取るメソッドで利用できる。

Condition を使えるメソッド

単一の値

MainTest.java
package sample.assertj;

import static org.assertj.core.api.Assertions.*;

import org.assertj.core.api.Condition;
import org.junit.Test;

public class MainTest {

    @Test
    public void 基本のis() throws Exception {
        assertThat("one").is(length(2));
    }

    @Test
    public void 否定のisNot() throws Exception {
        assertThat("one").isNot(length(3));
    }

    @Test
    public void notで否定もできる() throws Exception {
        assertThat("one").is(not(length(3)));
    }

    @Test
    public void allOfで全部の条件を満たすことを検証() throws Exception {
        assertThat("one").is(allOf(length(3), upperCase()));
    }

    @Test
    public void anyOfでいずれかの条件を満たすことを検証() throws Exception {
        assertThat("three").is(anyOf(length(3), upperCase()));
    }

    private static Length length(int length) {
        return new Length(length);
    }

    private static UpperCase upperCase() {
        return new UpperCase();
    }

    private static class Length extends Condition<String> {

        private int expectedLength;

        private Length(int expectedLength) {
            this.expectedLength = expectedLength;
        }

        @Override
        public boolean matches(String value) {
            return value.length() == this.expectedLength;
        }

        @Override
        public String toString() {
            return this.expectedLength + " 桁";
        }
    }

    private static class UpperCase extends Condition<String> {

        @Override
        public boolean matches(String value) {
            return value.matches("^[A-Z]+$");
        }

        @Override
        public String toString() {
            return "大文字";
        }
    }
}
実行結果
java.lang.AssertionError: 
Expecting:
 <"one">
to be <not :<3 桁>>
    at sample.assertj.MainTest.notで否定もできる(MainTest.java:22)

...

java.lang.AssertionError: 
Expecting:
 <"one">
to be <all of:<[3 桁, 大文字]>>
    at sample.assertj.MainTest.allOfで全部の条件を満たすことを検証(MainTest.java:27)

...

java.lang.AssertionError: 
Expecting:
 <"one">
not to be <3 桁>
    at sample.assertj.MainTest.否定のisNot(MainTest.java:17)

...

java.lang.AssertionError: 
Expecting:
 <"three">
to be <any of:<[3 桁, 大文字]>>
    at sample.assertj.MainTest.anyOfでいずれかの条件を満たすことを検証(MainTest.java:32)

...

java.lang.AssertionError: 
Expecting:
 <"one">
to be <2 桁>
    at sample.assertj.MainTest.基本のis(MainTest.java:12)

...
  • Hamcrest の Matcher より楽!
  • ちなみに、あくまで例のための実装なので、 assertThat("one").hasSize(3).matches("^[A-Z]+$"); とかすれば、 Condition なしでも検証はできる。
    • 既存の検証メソッドでは足りない場合や、同じ独自検証が複数箇所で登場する場合などに便利そう。

コレクション

MainTest.java
package sample.assertj;

import static org.assertj.core.api.Assertions.*;

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

import org.assertj.core.api.Condition;
import org.junit.Test;

public class MainTest {

    private List<String> list = Arrays.asList("one", "two", "three");

    @Test
    public void 基本のare() throws Exception {
        assertThat(list).are(length(3));
    }

    @Test
    public void 否定のareNot() throws Exception {
        assertThat(list).areNot(length(3));
    }

    @Test
    public void areAtLeastで最低何個の要素が条件を満たせば良いかを検証() throws Exception {
        assertThat(list).areAtLeast(2, length(5));
    }

    @Test
    public void areAtMostで最大何個の要素が条件を満たせば良いかを検証() throws Exception {
        assertThat(list).areAtMost(1, length(3));
    }

    @Test
    public void areExactlyで指定した個数の要素が条件を満たせば良いかを検証() throws Exception {
        assertThat(list).areExactly(3, length(3));
    }

    private static Length length(int length) {
        return new Length(length);
    }

    private static class Length extends Condition<String> {

        private int expectedLength;

        private Length(int expectedLength) {
            this.expectedLength = expectedLength;
        }

        @Override
        public boolean matches(String value) {
            return value.length() == this.expectedLength;
        }

        @Override
        public String toString() {
            return this.expectedLength + " 桁";
        }
    }
}
実行結果
java.lang.AssertionError: 
Expecting elements:
<["one", "two", "three"]>
 to be at most 1 times <3 桁>
    at sample.assertj.MainTest.areAtMostで最大何個の要素が条件を満たせば良いかを検証(MainTest.java:32)

...

java.lang.AssertionError: 
Expecting elements:
<["one", "two"]>
 of 
<["one", "two", "three"]>
 not to be <3 桁>
    at sample.assertj.MainTest.否定のareNot(MainTest.java:22)

...

java.lang.AssertionError: 
Expecting elements:
<["one", "two", "three"]>
 to be at least 2 times <5 桁>
    at sample.assertj.MainTest.areAtLeastで最低何個の要素が条件を満たせば良いかを検証(MainTest.java:27)

...

java.lang.AssertionError: 
Expecting elements:
<["three"]>
 of 
<["one", "two", "three"]>
 to be <3 桁>
    at sample.assertj.MainTest.基本のare(MainTest.java:17)

...

java.lang.AssertionError: 
Expecting elements:
<["one", "two", "three"]>
 to be exactly 3 times <3 桁>
    at sample.assertj.MainTest.areExactlyで指定した個数の要素が条件を満たせば良いかを検証(MainTest.java:37)

例外の検証

基本

MainTest.java
package sample.assertj;

import static org.assertj.core.api.Assertions.*;

import org.junit.Test;

public class MainTest {

    @Test
    public void test() {
        assertThatThrownBy(() -> { throw new NullPointerException(); })
            .isInstanceOf(IllegalArgumentException.class);
    }
}
実行結果
java.lang.AssertionError: 
Expecting:
 <java.lang.NullPointerException>
to be an instance of:
 <java.lang.IllegalArgumentException>
but was instance of:
 <java.lang.NullPointerException>
(以下略)
  • assertThatThrownBy() で例外の検証ができる。
  • 引数には、 ThrowingCallable インターフェースを実装したオブジェクトを渡す。
    • ThrowingCallable は関数型インタフェースなので、 Java8 以降ならラムダ式やメソッド参照を渡せる。

例外が投げられなかった場合の動作

MainTest.java
package sample.assertj;

import static org.assertj.core.api.Assertions.*;

import org.junit.Test;

public class MainTest {

    @Test
    public void test() {
        assertThatThrownBy(() -> {})
            .isInstanceOf(IllegalArgumentException.class);
    }
}
実行結果
java.lang.AssertionError: Expecting code to raise a throwable.
(以下略)
  • fail() とか要らない!

メッセージを検証する

MainTest.java
package sample.assertj;

import static org.assertj.core.api.Assertions.*;

import org.junit.Test;

public class MainTest {

    @Test
    public void test() {
        assertThatThrownBy(() -> {throw new NullPointerException("ぬるぽ");})
            .hasMessage("ヌルポ");
    }
}
実行結果
java.lang.AssertionError: 
Expecting message:
 <"ヌルポ">
but was:
 <"ぬるぽ">
(以下略)

原因例外の検証

MainTest.java
package sample.assertj;

import static org.assertj.core.api.Assertions.*;

import org.junit.Test;

public class MainTest {

    @Test
    public void 原因例外が同じクラスで_メッセージも同じなのでテストは成功する() {
        assertThatThrownBy(this::throwException)
            .hasCause(new NullPointerException("ぬるぽ"));
    }

    @Test
    public void 原因例外のクラスが違うのでテストは失敗する() {
        assertThatThrownBy(this::throwException)
            .hasCause(new RuntimeException("ぬるぽ"));
    }

    @Test
    public void 原因例外のメッセージが違うのでテストは失敗する() {
        assertThatThrownBy(this::throwException)
            .hasCause(new NullPointerException("ヌルポ"));
    }

    private void throwException() throws Exception {
        throw new Exception(new NullPointerException("ぬるぽ"));
    }
}
  • インスタンスが同じかどうかではなく、「同じ型の例外」かつ「同じメッセージ」かどうかで判定している。

検証処理を自作する

MyAssert.java
package sample.assertj;

import org.assertj.core.api.AbstractAssert;

public class MyAssert extends AbstractAssert<MyAssert, String> {

    public MyAssert(String actual) {
        super(actual, MyAssert.class);
    }

    public MyAssert isAlphabet() {
        this.isNotNull();

        if (!this.actual.matches("^[a-zA-Z]+$")) {
            this.failWithMessage("ピキピキ(#^ω^)アルファベットだっつてんだろ。(実際の値 = \"%s\")", this.actual);
        }

        return this;
    }
}

まず、検証処理を実装します。

MyAssertions.java
package sample.assertj;

public class MyAssertions {

    public static MyAssert assertThat(String actual) {
        return new MyAssert(actual);
    }
}

検証処理を呼び出すファサード的なクラスを作って。。。

MainTest.java
package sample.assertj;

import static sample.assertj.MyAssertions.*;

import org.junit.Test;

public class MainTest {

    @Test
    public void test() throws Exception {
        assertThat("にほんごヽ(゚∀゚)ノ").isAlphabet();
    }
}

使う。

実行結果
java.lang.AssertionError: ピキピキ(#^ω^)アルファベットだっつてんだろ。(実際の値 = "にほんごヽ(゚∀゚)ノ")
...

簡単!

説明

  • 自作の検証処理は、 AbstractAssert を継承して作る。
  • コンストラクタの書き方は、おまじないと思えば良いと思う。
  • 検証処理の先頭は、 isNotNull()null でないことを検証するのがお作法(ぬるぽで落ちるのを防ぐ)。
  • 検証対象の値は親クラスの actual フィールドに保存されている。
  • 検証の結果エラーになった場合は、 failWithMessage() メソッドでエラーメッセージを書き出す。
    • String.format() と同じノリでフォーマットできるもよう。
  • 検証メソッドは戻り値として自分自身のインスタンスを返すようにする(メソッドチェーンができるように)。

検証用クラスを自動生成する

検証用のクラスを、クラスごとに自動生成してくれるツールが用意されている。

コマンドラインツールと Maven のプラグインがデフォルトで提供されているようだが、 Gradle でもできなくはないみたいなので、 Gradle でやってみる。

フォルダ構成
|-src/
|  |-main/java/
|  |  `-MyClass.java ★こいつが対象のクラス
|  `-test/
|     |-java/
|     `-java-gen/ ★自動生成されたファイルはここに出力する
`-build.gradle
MyClass.java
package sample.assertj;

import java.util.List;

public class MyClass {

    private String string;
    private int number;
    private boolean bool;
    private List<String> list;

    getter, setter...
}
build.gradle
apply plugin: 'java'

sourceCompatibility = '1.8'
targetCompatibility = '1.8'
compileJava.options.encoding = 'UTF-8'
compileTestJava.options.encoding = 'UTF-8'

repositories {
    mavenCentral()
}

configurations {
    assertj // ★自動生成用に configurations を追加
}

dependencies {
    testCompile 'junit:junit:4.12'
    testCompile 'org.assertj:assertj-core:3.2.0'

    assertj 'org.assertj:assertj-assertions-generator:2.0.0' // ★自動生成用の依存関係
    assertj project(':') // ★自分自身のプロジェクトを指している
}

ext {
    assertjObject = file('src/test/java-gen') // ★自動生成される検証クラスの出力先
}

sourceSets {
    test.java {
        srcDir 'src/test/java'
        srcDir assertjObject.path // ★自動生成クラスの出力先を sourceSets に追加
    }
}

task assertjClean(type: Delete) { // ★自動生成されたファイルを削除するためのタスク
    delete assertjObject
}

task assertjGen(dependsOn: assertjClean, type: JavaExec) { // ★検証クラスを自動生成するタスク
    doFirst {
        if (!assertjObject.exists()) {
            assertjObject.mkdirs() // ★出力先がなければ作成
        }
    }

    // ★自動生成の処理を実行(Java コマンド)
    main 'org.assertj.assertions.generator.cli.AssertionGeneratorLauncher'
    classpath = files(configurations.assertj)
    workingDir = assertjObject
    args = [
        'sample.assertj' // ★生成したいクラスが存在するパッケージ、またはそのクラスの FQCN を直接指定する。
    ]
}

compileTestJava.dependsOn(assertjGen) // ★compileTestJava を動かしたら、勝手に自動生成タスクが動くように設定

できたら、 assertjGen タスクを実行する。

> gradle assertjGen
:assertjClean
:compileJava
:processResources UP-TO-DATE
:classes
:jar
:assertjGen
15:35:11.292 INFO  o.a.a.g.c.AssertionGeneratorLauncher - Generating assertions for classes [class sample.assertj.MyClass]
15:35:11.311 INFO  o.a.a.g.c.AssertionGeneratorLauncher - Generating assertions for class : sample.assertj.MyClass
15:35:11.329 INFO  o.a.a.g.c.AssertionGeneratorLauncher - Generated MyClass assertions file -> F:\tmp\github\Samples\java\assertj\src\test\java-gen\.\sample\assertj\MyClassAssert.java

BUILD SUCCESSFUL

Total time: 4.252 secs

生成されたクラスが以下。

MyClassAssert.java
package sample.assertj;

import org.assertj.core.api.AbstractAssert;
import org.assertj.core.internal.Iterables;
import org.assertj.core.util.Objects;

/**
 * {@link MyClass} specific assertions - Generated by CustomAssertionGenerator.
 */
public class MyClassAssert extends AbstractAssert<MyClassAssert, MyClass> {

  /**
   * Creates a new <code>{@link MyClassAssert}</code> to make assertions on actual MyClass.
   * @param actual the MyClass we want to make assertions on.
   */
  public MyClassAssert(MyClass actual) {
    super(actual, MyClassAssert.class);
  }

  /**
   * An entry point for MyClassAssert to follow AssertJ standard <code>assertThat()</code> statements.<br>
   * With a static import, one can write directly: <code>assertThat(myMyClass)</code> and get specific assertion with code completion.
   * @param actual the MyClass we want to make assertions on.
   * @return a new <code>{@link MyClassAssert}</code>
   */
  public static MyClassAssert assertThat(MyClass actual) {
    return new MyClassAssert(actual);
  }

  /**
   * Verifies that the actual MyClass's list contains the given String elements.
   * @param list the given elements that should be contained in actual MyClass's list.
   * @return this assertion object.
   * @throws AssertionError if the actual MyClass's list does not contain all given String elements.
   */
  public MyClassAssert hasList(String... list) {
    // check that actual MyClass we want to make assertions on is not null.
    isNotNull();

    // check that given String varargs is not null.
    if (list == null) failWithMessage("Expecting list parameter not to be null.");

    // check with standard error message, to set another message call: info.overridingErrorMessage("my error message");
    Iterables.instance().assertContains(info, actual.getList(), list);

    // return the current assertion for method chaining
    return this;
  }

  /**
   * Verifies that the actual MyClass's list contains <b>only<b> the given String elements and nothing else in whatever order.
   * @param list the given elements that should be contained in actual MyClass's list.
   * @return this assertion object.
   * @throws AssertionError if the actual MyClass's list does not contain all given String elements.
   */
  public MyClassAssert hasOnlyList(String... list) {
    // check that actual MyClass we want to make assertions on is not null.
    isNotNull();

    // check that given String varargs is not null.
    if (list == null) failWithMessage("Expecting list parameter not to be null.");

    // check with standard error message, to set another message call: info.overridingErrorMessage("my error message");
    Iterables.instance().assertContainsOnly(info, actual.getList(), list);

    // return the current assertion for method chaining
    return this;
  }

  /**
   * Verifies that the actual MyClass's list does not contain the given String elements.
   *
   * @param list the given elements that should not be in actual MyClass's list.
   * @return this assertion object.
   * @throws AssertionError if the actual MyClass's list contains any given String elements.
   */
  public MyClassAssert doesNotHaveList(String... list) {
    // check that actual MyClass we want to make assertions on is not null.
    isNotNull();

    // check that given String varargs is not null.
    if (list == null) failWithMessage("Expecting list parameter not to be null.");

    // check with standard error message (use overridingErrorMessage before contains to set your own message).
    Iterables.instance().assertDoesNotContain(info, actual.getList(), list);

    // return the current assertion for method chaining
    return this;
  }

  /**
   * Verifies that the actual MyClass has no list.
   * @return this assertion object.
   * @throws AssertionError if the actual MyClass's list is not empty.
   */
  public MyClassAssert hasNoList() {
    // check that actual MyClass we want to make assertions on is not null.
    isNotNull();

    // we override the default error message with a more explicit one
    String assertjErrorMessage = "\nExpecting :\n  <%s>\nnot to have list but had :\n  <%s>";

    // check
    if (actual.getList().iterator().hasNext()) {
      failWithMessage(assertjErrorMessage, actual, actual.getList());
    }

    // return the current assertion for method chaining
    return this;
  }


  /**
   * Verifies that the actual MyClass's number is equal to the given one.
   * @param number the given number to compare the actual MyClass's number to.
   * @return this assertion object.
   * @throws AssertionError - if the actual MyClass's number is not equal to the given one.
   */
  public MyClassAssert hasNumber(int number) {
    // check that actual MyClass we want to make assertions on is not null.
    isNotNull();

    // overrides the default error message with a more explicit one
    String assertjErrorMessage = "\nExpecting number of:\n  <%s>\nto be:\n  <%s>\nbut was:\n  <%s>";

    // check
    int actualNumber = actual.getNumber();
    if (actualNumber != number) {
      failWithMessage(assertjErrorMessage, actual, number, actualNumber);
    }

    // return the current assertion for method chaining
    return this;
  }

  /**
   * Verifies that the actual MyClass's string is equal to the given one.
   * @param string the given string to compare the actual MyClass's string to.
   * @return this assertion object.
   * @throws AssertionError - if the actual MyClass's string is not equal to the given one.
   */
  public MyClassAssert hasString(String string) {
    // check that actual MyClass we want to make assertions on is not null.
    isNotNull();

    // overrides the default error message with a more explicit one
    String assertjErrorMessage = "\nExpecting string of:\n  <%s>\nto be:\n  <%s>\nbut was:\n  <%s>";

    // null safe check
    String actualString = actual.getString();
    if (!Objects.areEqual(actualString, string)) {
      failWithMessage(assertjErrorMessage, actual, string, actualString);
    }

    // return the current assertion for method chaining
    return this;
  }

  /**
   * Verifies that the actual MyClass is bool.
   * @return this assertion object.
   * @throws AssertionError - if the actual MyClass is not bool.
   */
  public MyClassAssert isBool() {
    // check that actual MyClass we want to make assertions on is not null.
    isNotNull();

    // check
    if (!actual.isBool()) {
      failWithMessage("\nExpecting that actual MyClass is bool but is not.");
    }

    // return the current assertion for method chaining
    return this;
  }

  /**
   * Verifies that the actual MyClass is not bool.
   * @return this assertion object.
   * @throws AssertionError - if the actual MyClass is bool.
   */
  public MyClassAssert isNotBool() {
    // check that actual MyClass we want to make assertions on is not null.
    isNotNull();

    // check
    if (actual.isBool()) {
      failWithMessage("\nExpecting that actual MyClass is not bool but is.");
    }

    // return the current assertion for method chaining
    return this;
  }

}

各フィールドごとに hasXxx() という検証メソッドが作成されている。

こんな感じで使える。

MainTest.java
package sample.assertj;

import java.util.Arrays;

import org.junit.Test;

public class MainTest {

    @Test
    public void test() throws Exception {
        MyClass myClass = new MyClass();
        myClass.setString("hoge");
        myClass.setNumber(20);
        myClass.setList(Arrays.asList("hoge", "fuga", "piyo"));
        myClass.setBool(false);

        MyClassAssert
            .assertThat(myClass)
            .as("OK")
            .hasString("hoge")
            .hasNumber(20)
            .hasOnlyList("hoge", "fuga", "piyo")
            .isNotBool();
    }
}

Gradle プラグイン化してみました

上記の AssertJ のアサーションを自動生成するタスクをプラグイン化してみました。

Gradle - Plugin: com.github.opengl-BOBO.assertjGen

使い方は GitHub の README を参照ください。

参考