AssertJのIterableとArrayのアサーション

2022/04/04に公開

AssertJでIterableとArrayの様々なアサーションについてです。

こちらの記事はAssertJ -> Iterable and array assertionsの内容を日本語で極力わかりやすい形にしたものです。

contains

まずは各種containsアサーションについてです。
containsアサーションはリストに指定した値が含まれているかをテストするためのものです。
いくつか種類があるためそれぞれの違いを表にしてみました。

それぞれのアサーションでテストする内容

アサーション 要素数 並び順 重複 テスト内容
contains 検証しない 検証しない 検証しない リスト内に指定した値が全て含まれているか検証する
containsOnly 検証する 検証しない 検証しない リスト内に指定した値のみが存在することを検証する。リスト内で値が重複している場合は無視する。
containsExactly 検証する 検証する 検証しない リスト内に指定した値がその順番・数存在するかを検証する
containsExactlyInAnyOrder 検証する 検証しない 検証しない リスト内に指定した値が数も含めて存在するか検証する。順番は無視する。
containsSequence 検証しない 検証する 検証しない リストの中に指定した値が指定した順入っていることを検証する
containsSubsequence 検証しない 検証する 検証しない リストの中に指定した値が指定した順入っていることを検証する。
containsSequenceとの違いはcontainsSequenceでは指定した値の間に別の値があるとエラーになるが、こちらはエラーにならない。
containsOnlyOnce 検証しない 検証しない 検証する リストの中に指定した値は1つしかないことを検証する
containsAnyOf 検証しない 検証しない 検証しない リストの中に指定した値のうち1つだけでも存在するかを検証する

以下具体的なコードを書きます。
actualには List<String> actual = List.of("a", "b", "c", "a");をセットしています。

containsメソッド

1つ目のテストはa, bがactualに含まれるため成功します。
2つ目のテストはdはactualに含まれていないため失敗します。

// 通る
assertThat(actual).contains("a", "b");
// エラーになる
assertThat(actual).contains("d");

containsOnlyメソッド

1つ目のテストでは、a, b, cはactualに含まれていて、actualではaが重複していますが、containsOnlyでは重複は無視するため成功します。
2つ目のテストではactualにcが存在するため失敗します。

// 通る
assertThat(actual).containsOnly("a", "b", "c");
// エラーになる
assertThat(actual).containsOnly("a", "b");

containsExactlyメソッド

1つ目のテストでは、順番・値の内容ともに一致しているため成功します。
2つ目のテストでは、値自体は一致していますが、順番が一致していない(aが1,2番目にある想定がactualでは先頭と末尾にある)ため失敗します。

// 通る
assertThat(actual).containsExactly("a", "b", "c", "a");
// エラーになる
assertThat(actual).containsExactly("a", "a", "b", "c");

containsExactlyInAnyOrderメソッド

1つ目のテストでは、順番は一致していませんが値の内容と数は一致しているため成功します。
2つ目のテストでは、aの数が一致しない(actualにはaが2つある)ため失敗します。

// 通る
assertThat(actual).containsExactlyInAnyOrder("a", "a", "b", "c");
// エラーになる
assertThat(actual).containsExactlyInAnyOrder("a", "b", "c");

containsSequenceメソッド

1つ目のテストでは、actualにb,cが順番に(隣り合って)存在するため成功します。
2つ目のテストでは、actualでaとcは順番通りではあるものの、間にbが存在するため失敗します。

// 通る
assertThat(actual).containsSequence("b", "c");
// エラーになる
assertThat(actual).containsSequence("a", "c");

containsSubsequenceメソッド

1つ目のテストでは、actualにaとcの間にbが存在するものの、順番はあっているため成功します。
2つ目のテストでは、actualでc,bの順番にはなっていないため失敗します。

// 通る
assertThat(actual).containsSubsequence("a", "c");
// エラーになる
assertThat(actual).containsSubsequence("c", "b");

containsOnlyOnceメソッド

1つ目のテストでは、b,cはactualには1つしか存在しないため成功します。
2つ目のテストでは、aがactualに2つ存在するため失敗します。

// 通る
assertThat(actual).containsOnlyOnce("b", "c");
// エラーになる
assertThat(actual).containsOnlyOnce("a");

containsAnyOfメソッド

1つ目のテストでは、actualにdは存在しないが、aは存在するため成功します。
2つ目のテストでは、actualにd,eともに存在しないため失敗します。

// 通る
assertThat(actual).containsAnyOf("d", "a");
// エラーになる
assertThat(actual).containsAnyOf("d", "e");

Satisfy/Match

SatisfyアサーションとMatchアサーションではリスト内のフィールドに対してテストするためのものになります。

Satisfy

各種Satisfyメソッドの引数には、リストの値が入力値となるConsumerを指定して、Consumer内で対象の値クラスに対するテストを実施します。

アサーション テスト内容
allSatisfy リスト内全ての値がConsumer内のテストに成功すること
anySatisfy リスト内のうち1つの値以上がConsumer内のテストに成功すること
noneSatisfy リスト内全ての値がConsumer内のテストに失敗すること

実行結果です。

// allSatisfy
// 通る
assertThat(actual).allSatisfy(person -> {
  assertThat(person.name()).endsWith("郎");
});
// エラーになる
assertThat(actual).allSatisfy(person -> {
  assertThat(person.name()).isEqualTo("一郎");
});

// anySatisfy
// 通る
assertThat(actual).anySatisfy(person -> {
  assertThat(person.name()).isEqualTo("一郎");
});
// エラーになる
assertThat(actual).anySatisfy(person -> {
  assertThat(person.name()).isEqualTo("三郎");
});

// noneSatisfy
// 通る
assertThat(actual).noneSatisfy(person -> {
  assertThat(person.name()).isEqualTo("三郎");
});
// エラーになる
assertThat(actual).noneSatisfy(person -> {
  assertThat(person.name()).isEqualTo("一郎");
});

Match

各種Matchメソッドの引数には、リストの値が入力値となるFunctionを指定して、Functionの中では対象の値を抽出する処理を作成します。

アサーション テスト内容
allMatch リスト内全ての値がFunction内の条件に一致すること
anyMatch リスト内のうち1つの値以上がFunction内の条件に一致すること
noneMatch リスト内全ての値がFunction内の条件に一致しないこと

実行結果です。

// allMatch
// 通る
assertThat(actual).allMatch(person -> person.name().endsWith("郎"));
// エラーになる
assertThat(actual).allMatch(person -> person.name().equals("一郎"));

// anyMatch
// 通る
assertThat(actual).anyMatch(person -> person.name().equals("一郎"));
// エラーになる
assertThat(actual).anyMatch(person -> person.name().equals("三郎"));

// noneMatch
// 通る
assertThat(actual).noneMatch(person -> person.name().equals("三郎"));
// エラーになる
assertThat(actual).noneMatch(person -> person.name().equals("一郎"));

配列内の特定要素を取得する

テスト対象配列の指定位置にある要素を取得してアサーションする方法です。

指定位置の要素取得

指定位置の要素の取得はfirst last elementを利用します。

List<String> actual = List.of("a", "b", "c", "d");

assertThat(actual).first().isEqualTo("a");
assertThat(actual).last().isEqualTo("d");
assertThat(actual).element(1).isEqualTo("b");

型の明示

これらのメソッドで取得した要素は型が判断できないため、アサーションする際のメソッドにはObject型で渡せてしまいます。

List<String> actual = List.of("a", "b", "c", "d");

assertThat(actual).first().isEqualTo(1);

対策として2通りあります。
asを指定する方法とassertThatにアサーションクラスを渡す方法です。

List<String> actual = List.of("a", "b", "c", "d");

// asを指定する
assertThat(actual).first(as(STRING)).isEqualTo("a");

// assertThatにアサーションクラスを指定する
assertThat(actual, StringAssert.class).first().isEqualTo("1");

// 違いは型の強さのみであまり意識しなくても良さそうです。
assertThat(modelActual).first(as(INTEGER)).isEqualTo("a"); // 実行時エラーになる
// assertThat(actual, IntegerAssert.class).first().isEqualTo("1"); // コンパイルエラーになる

singleElement

最後に配列が1つのみを想定した処理です。

List<String> actual = List.of("a");
assertThat(actual).singleElement().isEqualTo("a");

以下のような場合はエラーになります。

List<String> actual = List.of("a", "b", "c", "d");
assertThat(actual).singleElement().isEqualTo("a");

// Expected size: 1 but was: 4 in: 
// ["a", "b", "c", "d"]
// java.lang.AssertionError: 
// が出力する

フィルタリング

フィルタリングは、テスト対象配列の中の一部のリストだけテストしたい(フィルタリングしたい)ような場合に、アサーションの中でフィルタリングしてそのままテストしまおうというものです。

例えば普通にフィルタリングをしてアサーションするような場合以下のような形になると思います。

List<filter.Person> actual = List.of(taro, ichiro, ziro);

var filterdActual = actual.stream()
      .filter(person -> person.age() >= 19)
      .toList();

assertThat(filterdActual).contains(ichiro);

フィルター処理とテストが分離してしまうので、テスト内容が追いにくくなってしまうかなと思います。
これを改善したものが今回のフィルタリング機能です。

filteredOn

filterdOnを利用すればassertThatの流れの中でフィルタリングもしてアサーションできます。

List<filter.Person> actual = List.of(taro, ichiro, ziro);

assertThat(actual).filteredOn(person -> person.age() >= 19)
      .contains(ichiro);

絞り込みとアサーションが一緒に見れるので、テスト内容がわかりやすく感じます。

filterdOnは以下のような書き方もできます。

List<Person> actual = List.of(taro, ichiro, ziro);

assertThat(actual).filteredOn("age", 19)
      .containsOnly(ichiro);

// notやinなども利用できる
assertThat(actual).filteredOn("age", not(19))
      .contains(taro);
assertThat(actual).filteredOn("age", in(19, 20))
      .contains(ichiro, ziro);
assertThat(actual).filteredOn("age", notIn(19, 20))
      .containsOnly(taro);

// メソッド参照を利用する
assertThat(actual).filteredOn(Person::age, 19)
      .containsOnly(ichiro);

filteredOnAssertions

filterをアサーションを利用して行いたい場合はfilteredOnAssertionsを利用します。

      List<filter.Person> actual = List.of(taro, ichiro, ziro);

      assertThat(actual).filteredOnAssertions(person -> assertThat(person.age()).isGreaterThan(19))
                        .containsOnly(ziro);

Condition

filter条件をConditionを利用してクラス化することもできます。

List<Person> actual = List.of(taro, ichiro, ziro);

Condition<Person> mvpStats= new Condition<Person>(person -> {
      return person.age() >= 20;
}, "adult");

assertThat(actual).filteredOn(mvpStats)
                  .containsOnly(ziro);

リスト内クラスの特定フィールドの比較

リストの比較のために、リスト内の特定のフィールドだけを抽出・リスト化してテストするのには手間がかかります。

List<extracting.Person> actual = List.of(taro, ichiro, ziro);

List<String> personNameList = actual.stream().map(extracting.Person::name).toList();
assertThat(personNameList).contains("太郎", "一郎", "二郎");

assertJではextractingという機能を利用して取得したいフィールド名を指定するだけで簡単にテストができます。

public record Person(String name, int age, Person friend) {}

List<Person> actual = List.of(taro, ichiro, ziro);

// nameを取得する
assertThat(actual).extracting("name")
                  .contains("太郎", "一郎", "二郎");
// 型を指定して取得する
assertThat(actual).extracting("name", String.class)
                  .contains("太郎", "一郎", "二郎");
// 子要素も取得できる
assertThat(actual).extracting("friend.name")
                  .contains("一郎");
// メソッド参照を利用して取得する
assertThat(actual).extracting(extracting.Person::name)
                  .contains("太郎", "一郎", "二郎");
// mapを利用して取得する
assertThat(actual).map(extracting.Person::name)
                  .contains("太郎", "一郎", "二郎");

複数フィールドのテスト

複数のフィールドを指定したい場合です。

List<Person> actual = List.of(taro, ichiro, ziro);

// 複数のフィールドをテストしたい場合は複数列挙してtupleで確認します
assertThat(actual).extracting("name", "age")
                  .contains(tuple("太郎", 18),
                            tuple("一郎", 19));
// 型を明確に指定します
assertThat(actual).extracting(extracting.Person::name, extracting.Person::age)
                  .contains(tuple("太郎", "aa"),
                            tuple("一郎", 19));

フィールド内のリストをテストする

リスト内要素のフィールドにリストを持っている場合にはflatExtractingを利用することでテストしやすくなります。

public record Person(String name, int age, List<String> favoriteFoods) {}

var taro = new flatExtracting.Person("太郎", 18, List.of("味噌汁", "シチュー"));
var ichiro = new flatExtracting.Person("一郎", 19, Collections.emptyList());
var ziro = new flatExtracting.Person("二郎", 20, List.of("カレー", "鯖の味噌煮"));

List<flatExtracting.Person> actual = List.of(taro, ichiro, ziro);

// 通常はこのような書き方をしないといけない
assertThat(actual).extracting("favoriteFoods")
                  .contains(List.of("味噌汁", "シチュー"),
                            List.of("カレー", "鯖の味噌煮"));

// flatExtractingであれば文字列を列挙するだけでよい
assertThat(actual).flatExtracting("favoriteFoods")
                  .contains("味噌汁", "シチュー", "カレー", "鯖の味噌煮");

比較方法のカスタマイズ

リスト内のクラスを比較する際に、既存のequalsメソッドではなく、usingElementComparatorを利用してアサーション用に独自に作成した比較方法を利用する方法になります。

var taro = new usingElementComparator.Person("太郎", 18);
var ichiro = new usingElementComparator.Person("一郎", 19);
var ziro = new usingElementComparator.Person("二郎", 20);

List<usingElementComparator.Person> actual = List.of(taro, ichiro, ziro);

// 通常は全ての項目が一致している必要がある(recordなので)
var expectTaro = new usingElementComparator.Person("太郎", 18);
assertThat(actual).contains(expectTaro);
// usingElementComparatorを利用することで比較方法をカスタマイズできる
// 本来であればageが異なるためエラーになるが、名前のみで比較するように設定しているためpassする
var expectIchiro = new usingElementComparator.Person("一郎", 20);
assertThat(actual).usingElementComparator((t1, t2) ->
                        t1.name().compareTo(t2.name()))
                  .contains(expectIchiro);

参考

AssertJ Document -> Iterable and array assertions
こちら
assertj-core javadoc -> AbstractObjectArrayAssert
assertj-core javadoc -> AbstractIterableAssert

Discussion