JUnit 5

Java単体テストテストフレームワークTDDJupiterプラットフォームアサーションライフサイクル

JUnit 5

概要

JUnit 5はJavaのための最も人気のある単体テストフレームワークです。JUnit 5(Jupiter)はモダンなJava機能を活用し、拡張可能なアーキテクチャを提供します。テスト駆動開発(TDD)を支援し、パラメータ化テスト、動的テスト、条件付きテスト実行などの高度な機能を備えています。

詳細

技術的特徴

  • JUnit Platform: テストエンジンの実行基盤
  • JUnit Jupiter: 新しいプログラミングモデルと拡張モデル
  • JUnit Vintage: JUnit 3/4ベーステストの後方互換性
  • Java 8+対応: ラムダ式やストリームAPIの活用
  • 柔軟なアサーション: 豊富なアサーションメソッドと詳細なエラーメッセージ

アーキテクチャの進化

JUnit 5は従来のJUnit 4から大幅に進化し、3つのサブプロジェクトで構成されています。Jupiter APIは新しいアノテーション体系を導入し、拡張モデルによってフレームワークの機能を柔軟に拡張できます。パラメータ化テストや動的テストなど、モダンなテスト手法をネイティブサポートしています。

エコシステム

Spring BootやGradle、Mavenなどの主要なJavaツールと深く統合されており、IDEサポートも充実しています。豊富な拡張ライブラリとサードパーティプラグインにより、様々なテストシナリオに対応できます。

メリット・デメリット

メリット

  1. モダンなJava機能活用: Java 8+の機能を最大限に活用
  2. 豊富なアサーション: 表現力豊かで読みやすいテストコード
  3. 柔軟な拡張性: カスタム拡張の作成が容易
  4. パラメータ化テスト: 複数のテストデータで効率的にテスト
  5. 条件付きテスト: 環境に応じたテスト実行制御
  6. 優れたIDE統合: 主要IDEでのネイティブサポート
  7. 詳細なエラー報告: デバッグに役立つ詳細な失敗情報

デメリット

  1. 学習コストの増加: JUnit 4からの移行時の学習負荷
  2. 設定の複雑さ: 高度な機能使用時の設定が複雑になる場合
  3. ライブラリサイズ: JUnit 4と比較してライブラリが大きい
  4. 移行コスト: 既存のJUnit 4テストコードの移行作業

参考ページ

書き方の例

Hello World(基本テスト)

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

class MyFirstJUnitJupiterTests {

    @Test
    void myFirstTest() {
        // 基本的なアサーション
        assertTrue(true);
        assertEquals(4, 2 + 2);
        assertNotNull("Hello World");
    }

    @Test
    void testWithCustomMessage() {
        int expected = 10;
        int actual = 5 + 5;
        assertEquals(expected, actual, "計算結果が期待値と一致しません");
    }
}

アサーション例

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;

class AssertionExamples {

    @Test
    void standardAssertions() {
        assertEquals(2, 1 + 1);
        assertTrue(1 > 0, "1は0より大きい");
        assertFalse(1 < 0, "1は0より小さくない");
        assertNull(null);
        assertNotNull("not null");
    }

    @Test
    void groupedAssertions() {
        // グループ化されたアサーション
        assertAll("person",
            () -> assertEquals("John", "John"),
            () -> assertEquals("Doe", "Doe")
        );
    }

    @Test
    void dependentAssertions() {
        // 依存関係のあるアサーション
        assertAll(
            () -> {
                String firstName = "John";
                assertNotNull(firstName);
                
                // 前のアサーションが成功した場合のみ実行
                assertAll("firstName",
                    () -> assertTrue(firstName.startsWith("J")),
                    () -> assertTrue(firstName.endsWith("n"))
                );
            }
        );
    }

    @Test
    void exceptionTesting() {
        // 例外のテスト
        Exception exception = assertThrows(
            IllegalArgumentException.class, 
            () -> Integer.parseInt("abc")
        );
        assertEquals("For input string: \"abc\"", exception.getMessage());
    }

    @Test
    void timeoutTesting() {
        // タイムアウトのテスト
        assertTimeout(Duration.ofMillis(100), () -> {
            Thread.sleep(50);
        });
    }
}

ライフサイクルメソッド

import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

class LifecycleMethodsTest {

    @BeforeAll
    static void initAll() {
        System.out.println("テストクラス開始前に一度実行");
    }

    @BeforeEach
    void init() {
        System.out.println("各テストメソッド開始前に実行");
    }

    @Test
    void succeedingTest() {
        assertTrue(true, "成功するテスト");
    }

    @Test
    void failingTest() {
        // このテストは意図的に失敗させる例
        // fail("意図的に失敗させたテスト");
        assertTrue(true, "実際は成功");
    }

    @Test
    @Disabled("このテストは無効化されています")
    void skippedTest() {
        // このテストはスキップされる
    }

    @AfterEach
    void tearDown() {
        System.out.println("各テストメソッド終了後に実行");
    }

    @AfterAll
    static void tearDownAll() {
        System.out.println("テストクラス終了後に一度実行");
    }
}

パラメータ化テスト

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;
import static org.junit.jupiter.api.Assertions.*;

class ParameterizedTestExamples {

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

    @ParameterizedTest
    @ValueSource(ints = {1, 2, 3, 5, 8, 13, 21})
    void testWithIntValues(int argument) {
        assertTrue(argument > 0 && argument < 25);
    }

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

    @ParameterizedTest
    @EnumSource(Month.class)
    void testWithEnumSource(Month month) {
        int quarter = (month.getValue() - 1) / 3 + 1;
        assertTrue(quarter >= 1 && quarter <= 4);
    }

    @ParameterizedTest
    @MethodSource("stringProvider")
    void testWithExplicitLocalMethodSource(String argument) {
        assertNotNull(argument);
    }

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

条件付きテスト

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.*;
import static org.junit.jupiter.api.Assertions.*;

class ConditionalTestExamples {

    @Test
    @EnabledOnOs(OS.MAC)
    void onlyOnMacOs() {
        // macOSでのみ実行
        assertTrue(System.getProperty("os.name").contains("Mac"));
    }

    @Test
    @EnabledOnOs({OS.LINUX, OS.MAC})
    void onLinuxOrMac() {
        // LinuxまたはmacOSでのみ実行
        assertTrue(true);
    }

    @Test
    @DisabledOnOs(OS.WINDOWS)
    void notOnWindows() {
        // Windows以外で実行
        assertTrue(true);
    }

    @Test
    @EnabledOnJre(JRE.JAVA_11)
    void onlyOnJava11() {
        // Java 11でのみ実行
        assertTrue(true);
    }

    @Test
    @EnabledIfSystemProperty(named = "os.arch", matches = ".*64.*")
    void onlyOn64BitArchitectures() {
        // 64ビットアーキテクチャでのみ実行
        assertTrue(true);
    }

    @Test
    @EnabledIfEnvironmentVariable(named = "ENV", matches = "staging-server")
    void onlyOnStagingServer() {
        // 特定の環境変数が設定されている場合のみ実行
        assertTrue(true);
    }
}

タグ付けとフィルタリング

import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

@Tag("fast")
class TaggedTests {

    @Test
    @Tag("taxes")
    void testingTaxCalculation() {
        assertTrue(true);
    }

    @Test
    @Tag("slow")
    @Tag("integration")
    void testingDatabaseQuery() {
        // データベースへの統合テスト
        assertTrue(true);
    }

    @Test
    @Tag("unit")
    void fastUnitTest() {
        // 高速な単体テスト
        assertEquals(4, 2 + 2);
    }
}

// 設定ファイルでのタグフィルタリング例
// junit-platform.properties:
// junit.jupiter.execution.parallel.enabled=true
// junit.jupiter.execution.parallel.mode.default=concurrent

// Gradle でのタグフィルタリング例:
// test {
//     useJUnitPlatform {
//         includeTags("fast", "unit")
//         excludeTags("slow", "integration")
//     }
// }