AssertJ

Java単体テストアサーション流暢APIテスト可読性JUnitTestNGBDD

単体テストツール

AssertJ

概要

AssertJは、Javaのための流暢で豊富なアサーションライブラリです。テストコードの可読性と保守性を大幅に向上させ、IDE(統合開発環境)での使いやすさを重視した設計となっています。JUnit、TestNG、その他のテストフレームワークと組み合わせて使用でき、強く型付けされた豊富なアサーションセットと、本当に役立つエラーメッセージを提供します。

詳細

AssertJは、従来のJUnitのアサーションメソッドの制限を克服するために開発されました。2024年の最新バージョンは3.27.2で、Java 8以上が必要です。単一のエントリーポイントassertThat()から始まる流暢なAPIを特徴とし、IDEのコード補完機能を最大限に活用できるよう設計されています。

AssertJの主な特徴:

  • 流暢なAPI: assertThat(actual).メソッド連鎖による直感的な記述
  • 豊富なアサーション: 文字列、コレクション、日付、ファイル、例外など様々な型に対応
  • 詳細なエラーメッセージ: 失敗時に具体的で理解しやすいメッセージを表示
  • IDE統合: コード補完により効率的なテスト作成をサポート
  • ソフトアサーション: 複数のアサーションエラーを収集
  • 再帰的比較: オブジェクトの深い比較機能
  • カスタムアサーション: 独自のアサーションを作成可能
  • BDDスタイル: 行動駆動開発に適したthen構文

AssertJは複数のモジュールを提供:

  • Core: JDK型(String、Iterable、Stream、Path、File、Map等)のアサーション
  • Guava: Guava型(Multimap、Optional等)のアサーション
  • Joda Time: Joda Time型のアサーション
  • Neo4J: Neo4J型のアサーション
  • DB: リレーショナルデータベース型のアサーション

メリット・デメリット

メリット

  1. 優れた可読性: 自然言語に近い流暢なAPIでテストの意図が明確
  2. IDE フレンドリー: コード補完により開発効率が大幅向上
  3. 豊富なアサーション: 多様なデータ型に対する専用アサーションメソッド
  4. 優秀なエラーメッセージ: 失敗の原因が一目で分かる詳細なメッセージ
  5. 学習コストの低さ: 統一されたAPIパターンで覚えやすい
  6. 柔軟性: JUnit、TestNG等の既存テストフレームワークと組み合わせ可能
  7. 活発な開発: 継続的な機能追加とバグ修正
  8. チェイン可能: 複数のアサーションを連鎖して記述可能

デメリット

  1. 外部依存: 追加のライブラリ依存関係が必要
  2. パフォーマンス: JUnitの基本アサーションと比較してわずかにオーバーヘッド
  3. 学習時間: 豊富な機能故に全体を理解するには時間が必要
  4. Java限定: 他の言語では使用不可
  5. メソッド数: 大量のメソッドによる選択肢の多さが時に混乱を招く

参考ページ

使い方の例

基本セットアップ

Maven依存関係

<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <version>3.27.2</version>
    <scope>test</scope>
</dependency>

Gradle依存関係

testImplementation("org.assertj:assertj-core:3.27.2")

基本的なimport

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

基本的なアサーション

数値のアサーション

@Test
void numberAssertions() {
    assertThat(42)
        .isEqualTo(42)
        .isNotEqualTo(41)
        .isGreaterThan(40)
        .isLessThan(50)
        .isBetween(40, 50)
        .isPositive()
        .isNotZero();
        
    assertThat(3.14)
        .isCloseTo(3.1, within(0.1))
        .isGreaterThan(3.0);
}

文字列のアサーション

@Test
void stringAssertions() {
    assertThat("Hello World")
        .isNotEmpty()
        .hasSize(11)
        .contains("World")
        .startsWith("Hello")
        .endsWith("World")
        .doesNotContain("Java")
        .matches("Hello.*")
        .isEqualToIgnoringCase("hello world");
}

Boolean のアサーション

@Test
void booleanAssertions() {
    assertThat(true).isTrue();
    assertThat(false).isFalse();
    
    String value = null;
    assertThat(value).isNull();
    
    String notNull = "test";
    assertThat(notNull).isNotNull();
}

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

リストのアサーション

@Test
void listAssertions() {
    List<String> fruits = Arrays.asList("apple", "banana", "cherry");
    
    assertThat(fruits)
        .hasSize(3)
        .contains("apple")
        .containsExactly("apple", "banana", "cherry")
        .containsExactlyInAnyOrder("cherry", "apple", "banana")
        .doesNotContain("orange")
        .startsWith("apple")
        .endsWith("cherry");
}

マップのアサーション

@Test
void mapAssertions() {
    Map<String, String> map = new HashMap<>();
    map.put("key1", "value1");
    map.put("key2", "value2");
    
    assertThat(map)
        .hasSize(2)
        .containsKey("key1")
        .containsValue("value1")
        .containsEntry("key1", "value1")
        .doesNotContainKey("key3");
}

例外のアサーション

例外の検証

@Test
void exceptionAssertions() {
    // Exception が発生することを検証
    assertThatThrownBy(() -> {
        throw new IllegalArgumentException("Invalid argument");
    })
    .isInstanceOf(IllegalArgumentException.class)
    .hasMessage("Invalid argument")
    .hasMessageContaining("Invalid");
    
    // Exception が発生しないことを検証
    assertThatCode(() -> {
        // 正常な処理
        String result = "test".toUpperCase();
    }).doesNotThrowAnyException();
}

特定の例外型の検証

@Test
void specificExceptionAssertions() {
    assertThatIllegalArgumentException().isThrownBy(() -> {
        throw new IllegalArgumentException("error");
    }).withMessage("error");
    
    assertThatNullPointerException().isThrownBy(() -> {
        String str = null;
        str.length();
    });
}

オブジェクトのアサーション

フィールド比較

public class Person {
    private String name;
    private int age;
    
    // constructors, getters, setters
}

@Test
void objectAssertions() {
    Person person = new Person("John", 30);
    
    assertThat(person)
        .extracting("name", "age")
        .containsExactly("John", 30);
        
    assertThat(person)
        .extracting(Person::getName)
        .isEqualTo("John");
}

再帰的比較

@Test
void recursiveComparison() {
    Person person1 = new Person("John", 30);
    Person person2 = new Person("John", 30);
    
    // 再帰的にフィールドを比較
    assertThat(person1)
        .usingRecursiveComparison()
        .isEqualTo(person2);
        
    // 特定フィールドを無視
    assertThat(person1)
        .usingRecursiveComparison()
        .ignoringFields("age")
        .isEqualTo(person2);
}

ソフトアサーション

複数エラーの収集

@Test
void softAssertions() {
    SoftAssertions softly = new SoftAssertions();
    
    String name = "John";
    int age = 25;
    
    softly.assertThat(name).isEqualTo("Jane");  // 失敗
    softly.assertThat(age).isEqualTo(30);       // 失敗
    softly.assertThat(name).isNotEmpty();      // 成功
    
    // すべてのアサーションを実行してからエラーをまとめて報告
    softly.assertAll();
}

@Test
void softAssertionsWithJUnit5() {
    assertSoftly(softly -> {
        softly.assertThat("John").isEqualTo("Jane");
        softly.assertThat(25).isEqualTo(30);
        softly.assertThat("John").isNotEmpty();
    });
}

日付とファイルのアサーション

日付のアサーション

@Test
void dateAssertions() {
    Date now = new Date();
    Date yesterday = Date.from(Instant.now().minus(1, ChronoUnit.DAYS));
    
    assertThat(now)
        .isAfter(yesterday)
        .isInThePast()  // 実際の現在時刻より前
        .isCloseTo(new Date(), 1000); // 1秒以内
}

ファイルのアサーション

@Test
void fileAssertions() throws IOException {
    File file = new File("test.txt");
    file.createNewFile();
    
    assertThat(file)
        .exists()
        .isFile()
        .canRead()
        .canWrite()
        .hasExtension("txt");
        
    file.delete();
}

カスタムアサーション

独自アサーションの作成

public class PersonAssert extends AbstractAssert<PersonAssert, Person> {
    
    public PersonAssert(Person actual) {
        super(actual, PersonAssert.class);
    }
    
    public static PersonAssert assertThat(Person actual) {
        return new PersonAssert(actual);
    }
    
    public PersonAssert hasName(String name) {
        isNotNull();
        if (!Objects.equals(actual.getName(), name)) {
            failWithMessage("Expected name to be <%s> but was <%s>", 
                name, actual.getName());
        }
        return this;
    }
    
    public PersonAssert isAdult() {
        isNotNull();
        if (actual.getAge() < 18) {
            failWithMessage("Expected person to be adult but was <%d> years old", 
                actual.getAge());
        }
        return this;
    }
}

@Test
void customAssertions() {
    Person person = new Person("John", 25);
    
    PersonAssert.assertThat(person)
        .hasName("John")
        .isAdult();
}

BDD スタイル

then 構文の使用

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

@Test
void bddStyleAssertions() {
    // Given
    String input = "hello";
    
    // When
    String result = input.toUpperCase();
    
    // Then
    then(result).isEqualTo("HELLO");
    then(result).isNotEqualTo("hello");
}

ストリームとOptionalのアサーション

Stream API との組み合わせ

@Test
void streamAssertions() {
    List<String> names = Arrays.asList("John", "Jane", "Bob");
    
    assertThat(names.stream())
        .filteredOn(name -> name.startsWith("J"))
        .hasSize(2)
        .containsExactly("John", "Jane");
        
    assertThat(names)
        .filteredOn(name -> name.length() > 3)
        .extracting(String::toUpperCase)
        .containsExactly("JOHN", "JANE");
}

Optional のアサーション

@Test
void optionalAssertions() {
    Optional<String> optional = Optional.of("value");
    Optional<String> empty = Optional.empty();
    
    assertThat(optional)
        .isPresent()
        .hasValue("value")
        .get().isEqualTo("value");
        
    assertThat(empty).isEmpty();
}