AssertJ
単体テストツール
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: リレーショナルデータベース型のアサーション
メリット・デメリット
メリット
- 優れた可読性: 自然言語に近い流暢なAPIでテストの意図が明確
- IDE フレンドリー: コード補完により開発効率が大幅向上
- 豊富なアサーション: 多様なデータ型に対する専用アサーションメソッド
- 優秀なエラーメッセージ: 失敗の原因が一目で分かる詳細なメッセージ
- 学習コストの低さ: 統一されたAPIパターンで覚えやすい
- 柔軟性: JUnit、TestNG等の既存テストフレームワークと組み合わせ可能
- 活発な開発: 継続的な機能追加とバグ修正
- チェイン可能: 複数のアサーションを連鎖して記述可能
デメリット
- 外部依存: 追加のライブラリ依存関係が必要
- パフォーマンス: JUnitの基本アサーションと比較してわずかにオーバーヘッド
- 学習時間: 豊富な機能故に全体を理解するには時間が必要
- Java限定: 他の言語では使用不可
- メソッド数: 大量のメソッドによる選択肢の多さが時に混乱を招く
参考ページ
使い方の例
基本セットアップ
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();
}