JUnit 5徹底解説:Javaテストの常識を変える新機能と使い方

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

この記事を読むことで、以下の知識を習得し、モダンなJavaテスト開発を実践できるようになります。

  • JUnit 5の基本的なテストクラスの書き方と、主要なアノテーションの役割を理解できる。
  • assertEqualsassertThrowsなど、JUnit 5が提供する豊富なアサーションメソッドを使いこなせるようになる。
  • @BeforeAll@BeforeEachといったアノテーションを駆使し、テストのライフサイクルを自由に制御できるようになる。
  • パラメータ化テスト(@ParameterizedTest)を用いて、DRY原則に則った効率的なテストコードを作成できる。
  • JUnit 4からJUnit 5へスムーズに移行するための重要な変更点と、その具体的な書き換え方法を学べる。

第1章: JUnit 5とは? – 新時代のJavaテストフレームワーク

JUnit 5は、Java開発における単体テストのデファクトスタンダードであるJUnitフレームワークの最新メジャーバージョンです。Java 8以降のモダンな機能(特にラムダ式)を全面的にサポートし、より柔軟で拡張性の高いテストを作成できるよう設計されています。 JUnit 4以前とは異なり、JUnit 5は3つの主要なサブプロジェクトから構成されるモジュール構造を採用しています。

JUnit 5の3つの構成要素

  • JUnit Platform: JVM上でテストフレームワークを起動するための基盤を提供します。IDEやビルドツールがテストを実行するための安定したインターフェースとしての役割を担います。
  • JUnit Jupiter: JUnit 5でテストを記述するための新しいプログラミングモデルと拡張モデルを提供します。@Test@BeforeEachといった我々が直接利用するアノテーションの多くは、このモジュールに含まれています。
  • JUnit Vintage: 既存のJUnit 3やJUnit 4で書かれたテストを、新しいJUnit Platform上で実行するためのテストエンジンです。これにより、古いテスト資産を無駄にすることなく、段階的にJUnit 5へ移行できます。

このモジュール化により、開発者は必要な機能だけを選択してプロジェクトに導入できるようになり、依存関係の管理がよりシンプルになりました。

JUnit 4からの進化

JUnit 5は、長年使われてきたJUnit 4の知見を活かしつつ、多くの点で改善が図られています。特に大きな違いは、アノテーションの整理、拡張モデルの刷新、そしてアサーションメソッドの強化です。

機能 JUnit 4 JUnit 5 主な変更点・メリット
テストメソッド @Test @Test アノテーション名は同じですが、JUnit 5ではexpectedtimeout属性が削除され、アサーションや専用の機能で代替します。
事前処理(クラス単位) @BeforeClass @BeforeAll 名称がより直感的になりました。デフォルトではstaticメソッドである必要があります。
事前処理(メソッド単位) @Before @BeforeEach こちらも、各テストの前に実行されることが明確な名称になりました。
事後処理(クラス単位) @AfterClass @AfterAll 名称がより直感的になりました。デフォルトではstaticメソッドである必要があります。
事後処理(メソッド単位) @After @AfterEach 各テストの後に実行されることが明確な名称になりました。
テストの無効化 @Ignore @Disabled 「無効化されている」という状態をより正確に表現する名称に変更されました。
例外テスト @Test(expected = ...) Assertions.assertThrows() 例外の型だけでなく、メッセージやプロパティの検証も可能になり、より詳細なテストが可能になりました。
拡張性 @RunWith, @Rule @ExtendWith (拡張モデル) 複数の拡張機能を組み合わせることが可能になり、非常に柔軟なテスト実装ができるようになりました。

第2章: 準備を始めよう – 環境構築と最初のテスト

JUnit 5を使い始めるには、ビルドツール(MavenまたはGradle)の依存関係を設定する必要があります。JUnit 5はJava 8以上を要求するため、実行環境も確認しておきましょう。

Mavenでの依存関係設定

Mavenプロジェクトでは、pom.xmlファイルに以下の依存関係を追加します。junit-jupiter-apiがテストコードを書くためのAPI、junit-jupiter-engineがそのテストを実行するためのエンジンです。

<dependencies>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <version>5.10.2</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-engine</artifactId>
        <version>5.10.2</version>
        <scope>test</scope>
    </dependency>
</dependencies>

また、maven-surefire-pluginがテストを正しく認識して実行できるように、プラグインの設定も確認しておくと良いでしょう。

Gradleでの依存関係設定

Gradleプロジェクトの場合、build.gradleファイルに以下のように記述します。Gradle 4.6以降ではJUnit 5がネイティブサポートされています。

plugins {
    id 'java'
}

repositories {
    mavenCentral()
}

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.2'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.2'
}

test {
    useJUnitPlatform()
}

最初のテストコード

準備が整ったら、最初のテストクラスを作成してみましょう。テストクラスやテストメソッドはpublicである必要がなくなりました。これはJUnit 4からの大きな変更点の一つです。 テストメソッドには@Testアノテーションを付与します。

Calculator.java (テスト対象クラス)

package com.example.project;

public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
}

CalculatorTest.java (テストクラス)

package com.example.project;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

class CalculatorTest {

    @Test
    void testAdd() {
        Calculator calculator = new Calculator();
        int result = calculator.add(2, 3);
        assertEquals(5, result, "2 + 3 should equal 5");
    }
}

この例では、Calculatorクラスのaddメソッドが正しく動作するかをテストしています。@Testアノテーションでこのメソッドがテストケースであることを示し、assertEqualsメソッドで期待値(5)と実際の結果(result)が等しいことを検証しています。


第3章: テストの心臓部 – アサーションを使いこなす

アサーションは、テストの結果が期待通りであるかを検証するための重要な機能です。JUnit 5では、org.junit.jupiter.api.Assertionsクラスに豊富なstaticメソッドとして提供されています。 これらをstatic importして使うのが一般的です。

基本的なアサーション

日常的に最もよく使われる基本的なアサーションメソッドをいくつか紹介します。

メソッド 説明
assertEquals(expected, actual) 期待値と実測値が等しい(equals()がtrue)ことを検証します。
assertNotEquals(unexpected, actual) 期待しない値と実測値が等しくないことを検証します。
assertTrue(condition) 条件(condition)がtrueであることを検証します。
assertFalse(condition) 条件(condition)がfalseであることを検証します。
assertNull(object) オブジェクトがnullであることを検証します。
assertNotNull(object) オブジェクトがnullでないことを検証します。
assertSame(expected, actual) 2つのオブジェクトが同じインスタンスを指していることを検証します(==での比較)。
assertNotSame(unexpected, actual) 2つのオブジェクトが異なるインスタンスを指していることを検証します。
assertArrayEquals(expected, actual) 2つの配列の要素がすべて等しいことを検証します。

例外のテスト: assertThrows

JUnit 4では@Test(expected=...)と記述していましたが、JUnit 5ではassertThrowsメソッドを使います。これにより、スローされた例外インスタンスを直接検証できるため、より詳細なテストが可能になりました。

@Test
void testDivideByZero() {
    Exception exception = assertThrows(ArithmeticException.class, () -> {
        // 0で除算するコード
        int result = 1 / 0;
    });

    // 例外メッセージの検証も可能
    assertEquals("/ by zero", exception.getMessage());
}

グループ化アサーション: assertAll

複数のアサーションを1つのグループとしてまとめ、すべてのアサーションを実行してから結果を報告したい場合があります。通常のアサーションでは、1つ目が失敗した時点でテストが中断されてしまいます。assertAllを使うと、すべてのチェックを実行し、失敗したものをまとめて報告してくれます。

@Test
void testUserProperties() {
    User user = new User("John", "Doe");
    assertAll("user",
        () -> assertEquals("John", user.getFirstName()),
        () -> assertEquals("Doe", user.getLastName())
    );
}

このようにラムダ式を使うことで、関連する一連の検証をまとめて記述でき、可読性が向上します。

パフォーマンスを意識したメッセージ生成

アサーションメソッドの最後の引数には、失敗時のメッセージを指定できます。このメッセージの生成にコストがかかる場合(例えば、複雑な文字列結合など)、ラムダ式で渡すことで、アサーションが成功した場合にはメッセージ生成の処理が実行されなくなり、パフォーマンスの向上が期待できます。

// 常にメッセージを生成
assertEquals(expected, actual, "Failure message: " + createExpensiveMessage());

// アサーション失敗時のみメッセージを生成
assertEquals(expected, actual, () -> "Failure message: " + createExpensiveMessage());

第4章: テストのライフサイクルを操る

JUnit 5では、テストクラスやテストメソッドの実行前後で特定の処理を実行するためのライフサイクルコールバック用のアノテーションが提供されています。 これらを使うことで、テストのセットアップ(初期化)やティアダウン(後処理)を効率的に管理できます。

ライフサイクルアノテーション

主要なライフサイクルアノテーションは以下の4つです。

アノテーション 実行タイミング JUnit 4での対応 備考
@BeforeAll クラス内の全てのテストが実行される前に一度だけ実行される。 @BeforeClass デフォルトではstaticメソッドである必要がある。
@AfterAll クラス内の全てのテストが実行された後に一度だけ実行される。 @AfterClass デフォルトではstaticメソッドである必要がある。
@BeforeEach 各テストメソッドが実行される直前に毎回実行される。 @Before テストごとに独立した初期状態を作るのに便利。
@AfterEach 各テストメソッドが実行された直後に毎回実行される。 @After リソースの解放など、テストごとの後片付けに使う。

実行順序の例

これらのアノテーションがどの順序で実行されるかを、具体的なコードで確認してみましょう。

import org.junit.jupiter.api.*;

class LifecycleDemoTest {

    @BeforeAll
    static void beforeAll() {
        System.out.println("--- BeforeAll: 1回だけ実行 ---");
    }

    @AfterAll
    static void afterAll() {
        System.out.println("--- AfterAll: 1回だけ実行 ---");
    }

    @BeforeEach
    void beforeEach() {
        System.out.println("  - BeforeEach: 各テストの前に実行");
    }

    @AfterEach
    void afterEach() {
        System.out.println("  - AfterEach: 各テストの後に実行");
    }

    @Test
    void test1() {
        System.out.println("    [Test 1]");
    }

    @Test
    void test2() {
        System.out.println("    [Test 2]");
    }
}

このテストを実行すると、コンソールには以下のような出力が得られます。

--- BeforeAll: 1回だけ実行 ---
  - BeforeEach: 各テストの前に実行
    [Test 1]
  - AfterEach: 各テストの後に実行
  - BeforeEach: 各テストの前に実行
    [Test 2]
  - AfterEach: 各テストの後に実行
--- AfterAll: 1回だけ実行 ---

テストインスタンスのライフサイクル

JUnitはデフォルトで、各@Testメソッドを実行するたびにテストクラスの新しいインスタンスを生成します。 これにより、テスト間の意図しない副作用を防いでいます。しかし、@TestInstance(Lifecycle.PER_CLASS)をクラスに付与することで、クラス全体で単一のインスタンスを共有するように変更できます。

このモードにすると、@BeforeAll@AfterAllメソッドをstaticにする必要がなくなり、インスタンスフィールドを使って状態を共有できるようになります。

テストの可読性を高めるアノテーション

  • @DisplayName: テストクラスやメソッドに、技術的な名前ではなく自然言語でわかりやすい名前を付けることができます。 レポート出力が見やすくなります。
  • @Disabled: 特定のテストメソッドやクラスを一時的に実行しないようにマークします。 理由を記述することも可能です。
  • @Nested: テストクラスの中に内部クラスを作ることで、テストを論理的なグループに構造化できます。 これにより、BBD(振る舞い駆動開発)スタイルのテストが書きやすくなります。
@DisplayName("電卓のテスト")
class CalculatorDisplayNameTest {

    @Nested
    @DisplayName("加算機能")
    class AdditionTests {

        @Test
        @DisplayName("正の数の足し算")
        void testPositive() {
            // ...
        }

        @Test
        @Disabled("TODO: 負の数のケースを実装")
        @DisplayName("負の数の足し算")
        void testNegative() {
            // ...
        }
    }
}

第5章: 発展的なテスト手法 – より高度なテストへ

JUnit 5は基本的なテスト機能に加えて、より効率的で表現力豊かなテストを記述するための高度な機能を提供します。

パラメータ化テスト (@ParameterizedTest)

同じテストロジックを異なる入力値で何度も実行したい場合、パラメータ化テストが非常に役立ちます。 これにより、テストコードの重複を大幅に削減できます。 @ParameterizedTestアノテーションを使い、パラメータのソースを指定するアノテーション(@ValueSource, @CsvSourceなど)と組み合わせて使用します。

@ValueSource

プリミティブ型(やString)の配列を直接パラメータとして渡す、最もシンプルな方法です。

@ParameterizedTest
@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
void palindromes(String candidate) {
    assertTrue(isPalindrome(candidate));
}

@CsvSource

カンマ区切りの文字列で複数の引数を指定できます。各文字列が1回のテスト実行に対応します。

@ParameterizedTest
@CsvSource({
    "apple,         1",
    "banana,        2",
    "'lemon, lime', 3"
})
void testWithCsvSource(String fruit, int rank) {
    assertNotNull(fruit);
    assertNotEquals(0, rank);
}

@MethodSource

より複雑なオブジェクトや動的に生成した値をパラメータとしたい場合、その値を返すstaticメソッドを指定します。メソッドはStreamIterableIteratorなどを返す必要があります。

static Stream<Arguments> stringProvider() {
    return Stream.of(
        Arguments.of("apple", 1),
        Arguments.of("banana", 2)
    );
}

@ParameterizedTest
@MethodSource("stringProvider")
void testWithMethodSource(String fruit, int rank) {
    // ...
}

繰り返しテスト (@RepeatedTest)

同じテストを単純に複数回繰り返したい場合に使います。例えば、タイミングに依存する問題や、ランダム性を含むコードのテストに役立つことがあります。

@RepeatedTest(5)
void repeatedTest(RepetitionInfo repetitionInfo) {
    System.out.println("Executing repeated test " + repetitionInfo.getCurrentRepetition());
    assertEquals(5, repetitionInfo.getTotalRepetitions());
}

動的テスト (@TestFactory)

動的テストは、実行時にテストケースを生成する全く新しい概念です。 @TestFactoryアノテーションを付与したメソッドは、DynamicNodeDynamicTestDynamicContainer)のコレクションまたはストリームを返す必要があります。 これにより、コンパイル時には定まらないデータに基づいて、柔軟にテストケース群を組み立てることが可能になります。

@TestFactory
Stream<DynamicTest> dynamicTestsFromStream() {
    return Stream.of("A", "B", "C")
        .map(str -> 
            DynamicTest.dynamicTest("Test " + str, () -> {
                // テストロジック
                assertTrue(str.length() == 1);
            })
        );
}

第6章: JUnit 4からの移行ガイド

既存のJUnit 4プロジェクトをJUnit 5に移行するプロセスは、多くの場合、それほど複雑ではありません。 JUnit Vintageエンジンのおかげで、古いテストと新しいテストを共存させながら、段階的に移行を進めることが可能です。

主要な変更点のまとめ

移行の際に特に注意すべき点を以下にまとめます。

  1. パッケージの変更: JUnit 5のアノテーションやクラスはorg.junit.jupiter.apiパッケージに属します。import文を修正する必要があります。 (例: org.junit.Testorg.junit.jupiter.api.Test)
  2. アノテーションの置換: 前述の通り、@Before@BeforeEachに、@BeforeClass@BeforeAllになるなど、多くのアノテーションの名前が変わっています。これらを機械的に置換していきます。
  3. アサーションの変更: org.junit.Assertクラスはorg.junit.jupiter.api.Assertionsクラスに変わります。メソッドのシグネチャ(引数の順序)も一部変更されている点に注意が必要です(例: メッセージ引数が最後に移動)。
  4. public修飾子が不要に: テストクラスやテストメソッドはpublicである必要がなくなりました。パッケージプライベートで宣言できます。
  5. ルール (@Rule, @ClassRule) の廃止: JUnit 4の@Rule@ClassRuleは、JUnit 5では拡張モデル (@ExtendWith) に置き換えられました。TemporaryFolderExpectedExceptionのような一般的なルールは、JUnit 5の組み込み機能やサードパーティの拡張ライブラリで代替します。

具体的な書き換え例: 例外テスト

JUnit 4

@Test(expected = ArithmeticException.class)
public void testDivideByZero() {
    int result = 1 / 0;
}

JUnit 5

@Test
void testDivideByZero() {
    assertThrows(ArithmeticException.class, () -> {
        int result = 1 / 0;
    });
}

移行は一度にすべてを行う必要はありません。新しいテストはJUnit 5で書き始め、既存のテストは時間があるときに少しずつリファクタリングしていくというアプローチが現実的です。


まとめ

JUnit 5は、単なるバージョンアップではなく、Javaのテスト文化を次のレベルへ引き上げるための強力なプラットフォームです。モジュール化されたアーキテクチャ、Java 8以降の機能を活かした表現力豊かなAPI、そして柔軟な拡張モデルは、私たちのテストコードをより堅牢で、保守しやすく、そして読みやすいものにしてくれます。

本記事では、基本的な使い方からパラメータ化テストや動的テストといった高度なトピック、さらにはJUnit 4からの移行パスまでを網羅的に解説しました。ここで得た知識を土台として、ぜひ実際のプロジェクトでJUnit 5を積極的に活用し、品質の高いソフトウェア開発を実践してください。

コメントを残す

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