JUnit 5
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サポートも充実しています。豊富な拡張ライブラリとサードパーティプラグインにより、様々なテストシナリオに対応できます。
メリット・デメリット
メリット
- モダンなJava機能活用: Java 8+の機能を最大限に活用
- 豊富なアサーション: 表現力豊かで読みやすいテストコード
- 柔軟な拡張性: カスタム拡張の作成が容易
- パラメータ化テスト: 複数のテストデータで効率的にテスト
- 条件付きテスト: 環境に応じたテスト実行制御
- 優れたIDE統合: 主要IDEでのネイティブサポート
- 詳細なエラー報告: デバッグに役立つ詳細な失敗情報
デメリット
- 学習コストの増加: JUnit 4からの移行時の学習負荷
- 設定の複雑さ: 高度な機能使用時の設定が複雑になる場合
- ライブラリサイズ: JUnit 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")
// }
// }