AssertJ完全ガイド:流れるようなアサーションでテストコードを革新する


この記事から得られる知識

  • AssertJの基本的な概念と、JUnit標準アサーションに対する優位性
  • MavenおよびGradleプロジェクトへのAssertJの導入方法
  • 基本的なアサーションから、文字列、数値、日付、コレクション、例外といった型特有のアサーションの具体的な使い方
  • as()を使用したカスタムエラーメッセージの作り方
  • SoftAssertionsを利用して、テストの失敗を一度にまとめて報告する方法
  • ドメインオブジェクトに対する独自の検証ルールを定義するカスタムアサーションの作成手順

はじめに:AssertJとは何か?

AssertJは、Javaのためのオープンソースの流暢なアサーションライブラリです。テストコードの可読性と保守性を劇的に向上させることを目的としています。 標準のJUnitが提供するassertEquals(expected, actual)のような形式とは異なり、AssertJは「流れるような(fluent)」インターフェースを採用しています。

これにより、assertThat(actual).is...()のように、自然言語に近い形で直感的にアサーションを記述できます。このアプローチは、コードを読むだけでテストの意図が明確に理解できるという大きな利点をもたらします。

JUnit標準アサーションとの比較

JUnit 5の標準アサーションを見てみましょう。

String actual = "AssertJ";
String expected = "AssertJ";
assertEquals(expected, actual);

List<String> list = Arrays.asList("Java", "Kotlin");
assertTrue(list.contains("Java"));

次に対応するAssertJのアサーションです。

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

String actual = "AssertJ";
assertThat(actual).isEqualTo("AssertJ");

List<String> list = Arrays.asList("Java", "Kotlin");
assertThat(list).contains("Java");

一見すると似ていますが、AssertJの利点は引数の順序を間違える心配がないこと、そしてIDEのコード補完機能との相性が抜群に良い点にあります。assertThat(list).と入力するだけで、リストに対して実行可能な全てのアサーションメソッドが候補として表示されます。これにより、どのような検証が可能かを迷うことなく、迅速にコードを記述できます。


セットアップ:プロジェクトへの導入

AssertJを使い始めるのは非常に簡単です。お使いのビルドツールに応じて、依存関係を追加するだけです。

Mavenの場合

pom.xmlファイルの<dependencies>セクションに以下の定義を追加します。testスコープに設定するのが一般的です。

<dependency>
  <groupId>org.assertj</groupId>
  <artifactId>assertj-core</artifactId>
  <!-- 最新バージョンを確認して指定してください -->
  <version>3.26.3</version>
  <scope>test</scope>
</dependency>

Gradleの場合

build.gradleファイルのdependenciesブロックに以下を追加します。

dependencies {
    // 最新バージョンを確認して指定してください
    testImplementation 'org.assertj:assertj-core:3.26.3'
}

依存関係を追加したら、テストクラスでorg.assertj.core.api.Assertionsクラスのメソッドをスタティックインポートすると、より簡潔に記述できるようになります。

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

基本のアサーション

AssertJの全てのアサーションはassertThat(actualValue)から始まります。このメソッドは、検証対象のオブジェクトを受け取り、その型に応じたアサーションメソッドを持つオブジェクトを返します。

頻繁に使用する基本マッチャー

以下は、オブジェクトの種類を問わず、日常的によく使われる基本的なアサーションメソッドの例です。

メソッド 説明 コード例
isEqualTo(expected) equals()メソッドを使用して、オブジェクトが期待値と等しいか検証します。 assertThat("hello").isEqualTo("hello");
isNotEqualTo(other) オブジェクトが指定された値と等しくないことを検証します。 assertThat("world").isNotEqualTo("hello");
isNull() オブジェクトがnullであることを検証します。 Object obj = null; assertThat(obj).isNull();
isNotNull() オブジェクトがnullでないことを検証します。 assertThat("not null").isNotNull();
isTrue() Boolean値がtrueであることを検証します。 assertThat(true).isTrue();
isFalse() Boolean値がfalseであることを検証します。 assertThat(false).isFalse();
isInstanceOf(Class) オブジェクトが指定されたクラスのインスタンスであることを検証します。 assertThat("string").isInstanceOf(String.class);
isSameAs(expected) オブジェクトが期待値と同一のインスタンスであるか(==での比較)を検証します。 Object a = new Object(); Object b = a; assertThat(b).isSameAs(a);

型別のアサーション:文字列、数値、日付

AssertJの真価は、特定のデータ型に特化した豊富なアサーションメソッドにあります。これにより、より表現力豊かなテストが可能になります。

文字列 (String) のアサーション

文字列の検証は非常によく行われます。AssertJはこれを簡単にするための多くのメソッドを提供します。

String text = "Hello, AssertJ World!";

assertThat(text)
    .isNotNull()
    .startsWith("Hello")
    .endsWith("World!")
    .contains("AssertJ")
    .doesNotContain("JUnit")
    .matches("Hello, .* World!")
    .hasSize(21);

数値 (Number) のアサーション

数値の比較も直感的に行えます。

int number = 42;
double floatingPoint = 3.14159;

assertThat(number)
    .isGreaterThan(40)
    .isLessThanOrEqualTo(42)
    .isPositive()
    .isEven();

// 浮動小数点数の比較には誤差を許容するisCloseToが便利
assertThat(floatingPoint).isCloseTo(3.14, within(0.01));

日付/時刻 (Date/Time) のアサーション

Java 8のDate and Time API(java.time)にも完全対応しています。

import java.time.LocalDate;

LocalDate today = LocalDate.now();
LocalDate yesterday = today.minusDays(1);
LocalDate tomorrow = today.plusDays(1);

assertThat(today)
    .isAfter(yesterday)
    .isBefore(tomorrow)
    .isBetween(yesterday, tomorrow)
    .isEqualTo(LocalDate.now());

コレクションとMapのアサーション

おそらくAssertJが最も輝く場面の一つが、コレクションやMapの検証です。ループを書いて各要素をチェックするような冗長なコードはもう必要ありません。

コレクション (List, Set)

リストやセットのサイズ、要素の有無、順序などを簡単に検証できます。

List<String> languages = Arrays.asList("Java", "Kotlin", "Scala", "Java");

assertThat(languages)
    .isNotNull()
    .isNotEmpty()
    .hasSize(4)
    .contains("Kotlin")
    .doesNotContain("Python");

// 要素の出現回数も確認できる
assertThat(languages).contains("Java", atLeastOnce());

// 重複を許さないSetに変換して検証
assertThat(languages)
    .as("重複を除いた言語リストの検証")
    .toSet()
    .hasSize(3);

contains, containsExactly, containsOnly の違い

コレクションの要素を検証する際、似て非なるこれらのメソッドの使い分けは重要です。

メソッド 説明
contains(values...) 指定された要素が少なくとも1つ以上含まれていることを検証します。順序は問わず、他の要素が含まれていても構いません。
containsExactly(values...) 指定された要素がその通り、その順序で含まれていることを検証します。要素の数、内容、順序が完全に一致する必要があります。
containsOnly(values...) 指定された要素のみが含まれていることを検証します。順序は問いませんが、要素の数と内容は完全に一致する必要があります。

extracting: オブジェクトのプロパティを抽出して検証

オブジェクトのリストを扱う際に絶大な効果を発揮するのがextracting()です。各オブジェクトから特定のプロパティを抽出し、そのプロパティのリストに対してアサーションを実行できます。

// Userクラス(nameとageプロパティを持つ)を想定
List<User> users = Arrays.asList(new User("Alice", 25), new User("Bob", 30));

// ユーザーリストから名前だけを抽出して検証
assertThat(users)
    .extracting("name")
    .contains("Alice", "Bob")
    .doesNotContain("Charlie");

// 複数のプロパティをタプルとして抽出して検証
assertThat(users)
    .extracting("name", "age")
    .contains(tuple("Alice", 25), tuple("Bob", 30));

Map

Mapのキー、値、エントリの検証も簡単です。

Map<String, String> capitals = new HashMap<>();
capitals.put("Japan", "Tokyo");
capitals.put("France", "Paris");

assertThat(capitals)
    .isNotEmpty()
    .hasSize(2)
    .containsKey("Japan")
    .containsValue("Paris")
    .doesNotContainKey("USA")
    .containsEntry("Japan", "Tokyo");

例外のアサーション

特定の条件下で例外が正しくスローされることをテストするのも重要な作業です。AssertJは、ラムダ式を使ってこれを簡潔に記述する方法を提供します。

基本的な例外テスト

assertThatThrownByassertThatExceptionOfType を使うことで、例外の型、メッセージ、原因などを検証できます。

// 何らかの例外をスローするメソッド
public void withdraw(int amount) {
    if (amount > balance) {
        throw new IllegalArgumentException("Insufficient funds");
    }
    // ...
}

// テストコード
assertThatThrownBy(() -> {
    account.withdraw(1000); // 残高を超える額を引き出す
}).isInstanceOf(IllegalArgumentException.class)
  .hasMessage("Insufficient funds");

// メッセージの一部を検証
assertThatThrownBy(() -> {
    account.withdraw(1000);
}).hasMessageContaining("funds");

// 特定の例外型に特化したアサーション
assertThatExceptionOfType(IOException.class).isThrownBy(() -> {
    // I/Oエラーを引き起こす処理
}).withMessage("%s not found", "file.txt")
  .withCauseInstanceOf(FileNotFoundException.class);

この方法を使えば、もはやJUnit 4の@Test(expected=...)アノテーションや、JUnit 5のassertThrowsの戻り値を変数に格納するといった手間は不要になります。


AssertJの便利な機能

基本的な使い方に加え、AssertJにはテストコードの品質をさらに向上させるための便利な機能が備わっています。

`as()`によるエラーメッセージのカスタマイズ

テストが失敗したとき、なぜ失敗したのかを即座に理解できることが重要です。as()メソッドを使うと、アサーションに説明を追加し、失敗時のエラーメッセージを分かりやすくカスタマイズできます。

User user = new User("Bob", 20);

// as() を使わない場合のエラーメッセージ
// org.junit.ComparisonFailure: expected:<true> but was:<false>

// as() を使った場合
assertThat(user.isAdmin())
    .as("ユーザー '%s' は管理者権限を持っているはず", user.getName())
    .isTrue();

// 失敗時のメッセージ:
// [ユーザー 'Bob' は管理者権限を持っているはず]
// expected: true
// but was: false

複雑なテストケースでは、この一言がデバッグの時間を大幅に短縮してくれます。

`SoftAssertions`による複数アサーションの実行

通常、テストメソッド内で最初のアサーションが失敗すると、その時点でテストは中止され、後続のアサーションは実行されません。しかし、一つのオブジェクトの複数のプロパティを一度に検証したい場合、これでは不便です。 SoftAssertionsは、この問題を解決します。すべてのアサーションを実行し、失敗したものをまとめて報告してくれます。

import org.assertj.core.api.SoftAssertions;

// ...

@Test
void userPropertiesShouldBeCorrect() {
    User user = new User("Alice", 99, "support@example.com");
    SoftAssertions soft = new SoftAssertions();

    soft.assertThat(user.getName()).isEqualTo("Bob"); // 失敗
    soft.assertThat(user.getAge()).isLessThan(100); // 成功
    soft.assertThat(user.getEmail()).endsWith("@test.com"); // 失敗

    // 全てのアサーションを評価し、失敗があれば報告する
    soft.assertAll();
}

このテストを実行すると、名前とEmailの両方のアサーションが失敗したことが一度に報告され、修正が効率的に行えます。JUnit 5のassertAllと似ていますが、AssertJの流暢なAPIスタイルで記述できるのが利点です。


カスタムアサーションの作成

プロジェクトが大きくなり、独自のドメインオブジェクト(例:User, Order, Product)を扱うようになると、繰り返し同じような検証を行う場面が増えてきます。このような場合、カスタムアサーションを作成することで、テストの可読性を飛躍的に高め、ドメイン固有の検証ロジックを再利用できます。

なぜカスタムアサーションが必要か?

例えば、Productというクラスがあり、それが「販売可能」である条件が「在庫が1以上あり、かつ非公開フラグが立っていない」だとします。カスタムアサーションがない場合、テストコードは次のようになります。

Product product = new Product("Laptop", 10, false);

assertThat(product.getStock()).isGreaterThan(0);
assertThat(product.isPrivate()).isFalse();

これでも問題はありませんが、「販売可能であること」というビジネスルールがコード上に直接表現されていません。カスタムアサーションを使うと、これを次のように書き換えられます。

// カスタムアサーションを使った場合の理想形
ProductAssert.assertThat(product).isSellable();

こちらのほうが、テストの意図が遥かに明確です。

カスタムアサーションの作り方

カスタムアサーションの作成は、3つのステップで行います。

  1. アサーションクラスの作成: AbstractAssert を継承したクラスを作成します。
  2. アサーションメソッドの実装: ドメイン固有の検証ロジックをメソッドとして実装します。
  3. エントリーポイントメソッドの作成: assertThat(YourObject) という形で呼び出せるようにします。

ステップ1&2:アサーションクラスとメソッドの実装

まず、AbstractAssert<S, A>を継承します。Sはアサーションクラス自身、Aは検証対象のオブジェクトの型です。

import org.assertj.core.api.AbstractAssert;
import org.assertj.core.api.Assertions;

// Productクラスは name, stock, isPrivate を持つと仮定
public class ProductAssert extends AbstractAssert<ProductAssert, Product> {

    public ProductAssert(Product actual) {
        super(actual, ProductAssert.class);
    }

    // エントリーポイントとなるstaticメソッド
    public static ProductAssert assertThat(Product actual) {
        return new ProductAssert(actual);
    }

    // カスタムアサーションメソッド
    public ProductAssert isSellable() {
        isNotNull(); // まずnullチェック

        if (actual.getStock() <= 0) {
            failWithMessage("Expected product to be sellable, but stock was <%s>", actual.getStock());
        }
        if (actual.isPrivate()) {
            failWithMessage("Expected product to be sellable, but it was private");
        }

        return this; // メソッドチェーンのためにthisを返す
    }
    
    public ProductAssert hasName(String name) {
        isNotNull();
        Assertions.assertThat(actual.getName())
            .overridingErrorMessage("Expected product's name to be <%s> but was <%s>", name, actual.getName())
            .isEqualTo(name);
        return this;
    }
}

ステップ3:カスタムアサーションの利用

作成したカスタムアサーションは、staticメソッドを呼び出すだけで使えます。

import static com.example.asserts.ProductAssert.assertThat;

// ...

@Test
void productShouldBeSellable() {
    Product sellableProduct = new Product("Keyboard", 5, false);
    Product notSellableProduct = new Product("Mouse", 0, false);
    Product privateProduct = new Product("Monitor", 10, true);

    // カスタムアサーションで可読性の高いテストを実現
    assertThat(sellableProduct)
        .isSellable()
        .hasName("Keyboard");
        
    // 失敗するケースのテスト
    assertThatThrownBy(() -> assertThat(notSellableProduct).isSellable());
    assertThatThrownBy(() -> assertThat(privateProduct).isSellable());
}

このように、カスタムアサーションはテストコードをドメインの言葉で語らせるための強力なツールです。適切に活用することで、テストは単なるコードの正しさの検証から、仕様書そのものへと昇華させることができます。


まとめ

AssertJは、Javaにおけるテストコードの記述を、より直感的で、表現力豊かで、楽しいものに変えてくれる強力なライブラリです。流れるようなAPIはコードの可読性を高め、豊富なアサーションメソッドは定型的な検証コードを削減します。

特に、コレクションの操作、例外の検証、そしてカスタムアサーションによるドメイン固有のルールの表現は、大規模なアプリケーションのテスト品質と保守性を維持する上で非常に価値があります。

もし、まだJUnitの標準アサーションのみを使用しているのであれば、ぜひ次のプロジェクトでAssertJの導入を検討してみてください。一度その書きやすさと表現力に慣れてしまうと、もう元には戻れなくなることでしょう。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です