概要
JUnit 5 の assertEquals や assertTrue でテストを書いていると、「期待値と実際値のどちらが先だったか」「コレクションの中身をどう検証すればいいか」で手が止まることがあります。AssertJ は assertThat(actual).isEqualTo(expected) の形で主語を先に書く流暢な API を提供し、IDE の補完と組み合わせることで「何を検証しているか」がテストコードを読むだけで分かるようになります。この記事では、注文処理の結果検証を題材に、リストの要素検証(extracting + containsExactly)、例外メッセージの検証(assertThatThrownBy)、文字列の部分一致検証を一通り扱います。JUnit 5 標準との書き方の違いも示すので、チームへの導入判断にも使えるはずです。特にコレクションの中身を細かく検証する場面では、AssertJ で劇的に読みやすくなります。
使いどころ
注文一覧 API のレスポンスに含まれる商品名と数量を extracting + containsExactly でまとめて検証する
バリデーションエラー時の例外メッセージに必要な情報が含まれていることを assertThatThrownBy で検証する
帳票出力の結果文字列がヘッダー・明細・フッターの順に構成されていることを contains / startsWith / endsWith で検証する
ドメインオブジェクトの複数フィールドを satisfies や extracting で読みやすく確認する
リストの順序が仕様に含まれる検索結果で、containsExactly と hasSize を組み合わせて回帰を防ぐ
コード例
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import java.util.List;
import static org.assertj.core.api.Assertions.*;
class OrderValidationTest {
record OrderItem(String productName, int quantity, int unitPrice) {
int subtotal() { return quantity * unitPrice; }
}
record Order(String orderId, List<OrderItem> items) {
int totalAmount() {
return items.stream().mapToInt(OrderItem::subtotal).sum();
}
String summary() {
return "注文番号: %s / 合計: %,d円 / 明細数: %d"
.formatted(orderId, totalAmount(), items.size());
}
}
static Order createSampleOrder() {
return new Order("ORD-2024-001", List.of(
new OrderItem("ボールペン(黒)", 10, 150),
new OrderItem("コピー用紙 A4", 5, 480),
new OrderItem("付箋(大)", 3, 320)));
}
@Nested @DisplayName("コレクション検証")
class CollectionTests {
@Test @DisplayName("商品名と数量を一括検証する")
void extractFields() {
assertThat(createSampleOrder().items())
.hasSize(3)
.extracting(OrderItem::productName, OrderItem::quantity)
.containsExactly(
tuple("ボールペン(黒)", 10),
tuple("コピー用紙 A4", 5),
tuple("付箋(大)", 3));
}
}
@Nested @DisplayName("文字列検証")
class StringTests {
@Test @DisplayName("サマリーに必要な情報が含まれている")
void summaryContainsInfo() {
assertThat(createSampleOrder().summary())
.startsWith("注文番号: ORD-2024-001")
.contains("4,860円")
.contains("明細数: 3");
}
}
@Nested @DisplayName("例外検証")
class ExceptionTests {
static Order createOrder(String orderId, List<OrderItem> items) {
if (orderId == null || orderId.isBlank()) throw new IllegalArgumentException("注文番号は必須です");
if (items == null || items.isEmpty()) throw new IllegalArgumentException("明細が空の注文は作成できません");
return new Order(orderId, items);
}
@Test @DisplayName("注文番号が空だと例外が発生する")
void throwsWhenBlank() {
assertThatThrownBy(() -> createOrder("", List.of()))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("注文番号は必須です");
}
}
}Version Coverage
record と組み合わせると extracting(Order::productName) のようにメソッド参照で型安全にフィールドを取り出せる。
// Java 17: record でテストデータ準備が簡潔
record Order(String productName, int quantity) {}
assertThat(orders)
.extracting(Order::productName, Order::quantity)
.containsExactly(tuple("ボールペン", 10), tuple("ノート", 5));Library Comparison
注意点
assertThat は org.assertj.core.api.Assertions.assertThat をインポートすること。Hamcrest の assertThat と混同しやすい
isEqualTo は equals() で比較する。record なら自動生成される equals で比較されるが、通常クラスでは equals を実装していないと参照比較になる
extracting でフィールドを取り出す際、メソッド参照(Order::productName)を使うと型安全になる。文字列指定はリファクタリングに弱い
assertThatThrownBy に渡すラムダの中で例外が発生しなかった場合、AssertJ が AssertionError を投げる
AssertJ は JUnit 5 と併用するライブラリであり、テストランナーの代替ではない。@Test は JUnit 5 のものを引き続き使う
containsExactlyInAnyOrder は順序を無視するため、ソート仕様を確認したいテストでは不適切になる
便利なアサーションを使いすぎると、かえってチーム内で読みにくい場合がある。頻出パターンから段階的に導入する
FAQ
技術的には問題ありませんが、チーム内でスタイルが混在すると可読性が下がります。導入するなら全テストで統一する方針が望ましいです。
containsExactly は順序も含めて一致を検証します。containsExactlyInAnyOrder は順序を問わず要素の一致だけを検証します。
同じ検証パターンが複数テストに重複するときに AbstractAssert を継承して作ります。3回以上同じ検証を書いたら抽出を検討する目安です。
コレクション、Optional、例外、文字列のように複数条件を読みやすく並べたい場面で効果が大きいです。単純な数値比較だけなら JUnit 標準でも十分です。
DTO や画面レスポンスの複数フィールドをまとめて確認したい場面では有効です。ただし unrelated な検証まで1テストに詰め込むと意図がぼやけるため、対象を絞って使います。