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
インストール
testCompile 'org.assertj:assertj-core:3.2.0'
実装
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 の場合は、「インポートの編成」と「お気に入り」を変更することで対応できる。
「インポートの編成」を変更する
- [ウィンドウ] → [設定]
- [Java] → [コード・スタイル] → [インポートの編成]
- 「.* に必要な静的インポート数」の設定値を
1
にする。
「お気に入り」を変更する
- [ウィンドウ] → [設定]
- [Java] → [エディター] → [コンテンツ・アシスト] → [お気に入り]
- [新規タイプ] ボタンをクリック
- 「完全修飾型を入力」に
org.assertj.core.api.Assertions
と入力して [OK]。
すでに JUnit や Hamcrest 用の設定がある場合は、 assertThat
が競合する。
完全に AssertJ に移行するなら、外しておいたほうがいいかもしれない。
これで assertThat
とエディタ上で入力すれば、 Assertions
のメソッドが補完候補として表示され、入力を確定させれば勝手に Assertions
のメンバーを静的インポートしてくれるようになり捗る。
他の IDE (NetBeans, IntelliJ Idea など)も同じような設定があるはず。たぶん。
検証に説明を追加する
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()
の直後に書くようにしておくといい。
途中で検証が失敗しても残りの検証を続行させる
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
を使う
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()
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()
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()
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()
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()
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()
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()
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()
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()
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()
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()
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()
を使う。
- 間を飛ばしても検証 OK にしたい場合は
- 期待値に
List
などを渡したい場合は...ない?
条件を満たす要素だけを検証する - filteredOn()
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()
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()
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()
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()
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()
abcdefg
hijklmn
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()
あいうえお
かきくけこ
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
基本
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 を使えるメソッド
単一の値
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 なしでも検証はできる。- 既存の検証メソッドでは足りない場合や、同じ独自検証が複数箇所で登場する場合などに便利そう。
コレクション
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)
例外の検証
基本
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 以降ならラムダ式やメソッド参照を渡せる。
-
例外が投げられなかった場合の動作
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()
とか要らない!
メッセージを検証する
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:
<"ぬるぽ">
(以下略)
原因例外の検証
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("ぬるぽ"));
}
}
- インスタンスが同じかどうかではなく、「同じ型の例外」かつ「同じメッセージ」かどうかで判定している。
自作の例外を検証できるようにする
public class MyException extends RuntimeException {
private final String code;
private MyException(String message, String code) {
super(message);
this.code = code;
}
public String getCode() {
return code;
}
}
@Test
void test() {
assertThatThrownBy(() -> {
throw new MyException("test-message", "test-code");
})
.isInstanceOf(MyException.class)
.hasMessage("test-message")
.satisfies((Throwable th) -> {
// Cannot resolve method 'getCode' in 'Throwable'
assertThat(th.getCode()).isEqualTo("test-code");
});
}
-
assertThatThrownBy
を使うと、戻り値の型はAbstractThrowableAssert<?, ? extends Throwable>
になっており、例外の型はThrowable
としてしか扱えない状態になっている - このため、
satisfies
やextracting
で自作型が持つ独自のプロパティなどにアクセスしようと思ってもできない - 例外の型を維持したままアサーションをしたい場合は、以下のようにする
@Test
void test() {
assertThatExceptionOfType(MyException.class)
.isThrownBy(() -> {
throw new MyException("test-message", "test-code");
})
.withMessage("test-message")
.satisfies((MyException e) -> {
assertThat(e.getCode()).isEqualTo("test-code");
});
}
-
assertThatExceptionOfType
で、想定される例外のClass
を指定する - 続いて
isThrownBy
で、例外をスローする予定の処理を渡す - メッセージの検証などが
hasMessage
からwithMessage
になっているなど若干の違いはあるが、だいたい同じような感じで検証できる -
satisfies
やextracting
は最初に渡した例外の型で扱われるようになるので、独自の例外型だけがもつプロパティの検証などができるようになる
検証処理を自作する
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;
}
}
まず、検証処理を実装します。
package sample.assertj;
public class MyAssertions {
public static MyAssert assertThat(String actual) {
return new MyAssert(actual);
}
}
検証処理を呼び出すファサード的なクラスを作って。。。
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
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...
}
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
生成されたクラスが以下。
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()
という検証メソッドが作成されている。
こんな感じで使える。
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 を参照ください。