はじめに

TDD(テスト駆動開発)を実践するには、テストを素早く書いて実行できる環境が不可欠です。Red-Green-Refactorサイクルを円滑に回すためには、テストの実行が数秒で完了し、結果がすぐにフィードバックされる環境を整える必要があります。

Javaにおける最も標準的なテスティングフレームワークはJUnitです。特にJUnit 5(JUnit Jupiter) は、JUnit 4から大幅に進化し、モダンなテスト記述スタイル、拡張モデル、パラメータ化テストなど、TDDを効率的に実践するための機能を豊富に備えています。

この記事では、MavenとGradleそれぞれでJUnit 5の環境を構築し、IntelliJ IDEAとVS Codeでテストを実行する手順を解説します。さらに、TDDを始めるために最低限知っておくべきアノテーションとAssertionsの基本も紹介します。

前提条件

本記事の手順を実行するには、以下の環境が必要です。

項目 要件
Java JDK 17以上(JUnit 5.11以降はJava 17が必須)
ビルドツール Maven 3.6以上 または Gradle 4.6以上
IDE IntelliJ IDEA 2023.1以上 または VS Code + Extension Pack for Java

JUnit 5はJava 17以上を必要とします。JDKのバージョンが古い場合は、事前にアップデートしてください。

JUnit 5のアーキテクチャ

JUnit 5は、従来のJUnitと異なり、3つのサブプロジェクトで構成されています。

flowchart TB
    subgraph JUnit5["JUnit 5"]
        JP["JUnit Platform<br/>テスト実行基盤"]
        JJ["JUnit Jupiter<br/>新しいテストAPI"]
        JV["JUnit Vintage<br/>JUnit 3/4互換"]
    end
    
    JP --> JJ
    JP --> JV
    
    style JP fill:#e6f3ff,stroke:#0066cc,color:#000000
    style JJ fill:#e6ffe6,stroke:#00cc00,color:#000000
    style JV fill:#fff2e6,stroke:#cc6600,color:#000000
コンポーネント 役割
JUnit Platform IDEやビルドツールからテストを起動するための基盤
JUnit Jupiter JUnit 5でテストを書くためのAPI(@TestAssertionsなど)
JUnit Vintage JUnit 3/4で書かれた既存テストを実行するためのエンジン

TDDで新規プロジェクトを始める場合は、JUnit Jupiterのみを依存関係に追加すれば十分です。

Mavenでの環境構築

プロジェクトの作成

まず、Mavenプロジェクトを作成します。以下のコマンドを実行してください。

1
2
3
4
5
6
mvn archetype:generate \
  -DgroupId=com.example \
  -DartifactId=tdd-demo \
  -DarchetypeArtifactId=maven-archetype-quickstart \
  -DarchetypeVersion=1.5 \
  -DinteractiveMode=false

pom.xmlの設定

生成されたpom.xmlを開き、JUnit 5の依存関係とMaven Surefire Pluginを設定します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>tdd-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <junit.version>5.11.4</junit.version>
    </properties>

    <dependencies>
        <!-- JUnit Jupiter(JUnit 5のテストAPI) -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- Maven Surefire Plugin: テスト実行に必須 -->
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.5.4</version>
            </plugin>
        </plugins>
    </build>
</project>

junit-jupiterアーティファクトを追加するだけで、JUnit Jupiter APIとエンジンの両方が含まれます。Maven Surefire Plugin 3.0以上は、JUnit Platformをネイティブでサポートしているため、追加設定なしでJUnit 5のテストを実行できます。

BOM(Bill of Materials)を使用する場合

複数のJUnitアーティファクトを使用する場合は、BOMを使ってバージョンを一元管理できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.junit</groupId>
            <artifactId>junit-bom</artifactId>
            <version>5.11.4</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <scope>test</scope>
        <!-- BOM使用時はバージョン指定不要 -->
    </dependency>
</dependencies>

テストの実行

以下のコマンドでテストを実行します。

1
2
3
4
5
6
7
8
# すべてのテストを実行
mvn test

# 特定のテストクラスのみ実行
mvn test -Dtest=CalculatorTest

# 特定のテストメソッドのみ実行
mvn test -Dtest=CalculatorTest#shouldAddTwoNumbers

Gradleでの環境構築

プロジェクトの作成

Gradleプロジェクトを作成します。

1
2
mkdir tdd-demo && cd tdd-demo
gradle init --type java-application --dsl kotlin --test-framework junit-jupiter

build.gradle.ktsの設定(Kotlin DSL)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
plugins {
    java
}

group = "com.example"
version = "1.0-SNAPSHOT"

java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(17))
    }
}

repositories {
    mavenCentral()
}

dependencies {
    // JUnit Jupiter
    testImplementation("org.junit.jupiter:junit-jupiter:5.11.4")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

tasks.test {
    useJUnitPlatform()
    testLogging {
        events("passed", "skipped", "failed")
    }
}

build.gradleの設定(Groovy DSL)

Groovy DSLを使用する場合は以下のように設定します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
plugins {
    id 'java'
}

group = 'com.example'
version = '1.0-SNAPSHOT'

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

repositories {
    mavenCentral()
}

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter:5.11.4'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

test {
    useJUnitPlatform()
    testLogging {
        events 'passed', 'skipped', 'failed'
    }
}

useJUnitPlatform()の指定がテスト実行に必須です。この設定がないと、GradleのtestタスクがJUnit 5のテストを認識しません。

テストの実行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# すべてのテストを実行
./gradlew test

# 特定のテストクラスのみ実行
./gradlew test --tests CalculatorTest

# 特定のテストメソッドのみ実行
./gradlew test --tests "CalculatorTest.shouldAddTwoNumbers"

# テストを強制的に再実行(キャッシュを無視)
./gradlew test --rerun-tasks

IDEでのテスト実行

IntelliJ IDEAの場合

IntelliJ IDEAはJUnit 5をネイティブでサポートしています。特別なプラグインは不要です。

  1. プロジェクトをインポート(pom.xmlまたはbuild.gradle.ktsを開く)
  2. Mavenの場合はReload All Maven Projects、Gradleの場合はReload All Gradle Projectsを実行
  3. テストクラスを開き、クラス名またはメソッド名の横にある再生ボタンをクリック

キーボードショートカットを使用すると、さらに効率的にテストを実行できます。

操作 Windows/Linux macOS
カーソル位置のテストを実行 Ctrl + Shift + F10 Ctrl + Shift + R
前回実行したテストを再実行 Shift + F10 Ctrl + R
デバッグモードでテスト実行 Shift + F9 Ctrl + D

VS Codeの場合

VS CodeでJUnit 5のテストを実行するには、Extension Pack for Javaをインストールします。

  1. 拡張機能から「Extension Pack for Java」をインストール
  2. プロジェクトフォルダを開く
  3. テストクラスを開くと、@Testアノテーションの上に「Run Test」「Debug Test」のコードレンズが表示される
  4. クリックしてテストを実行

サイドバーの「Testing」アイコンからテストエクスプローラーを開くと、プロジェクト内のすべてのテストを一覧表示し、一括実行することもできます。

ディレクトリ構造

Javaの標準的なプロジェクト構造では、テストコードはsrc/test/javaディレクトリに配置します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
tdd-demo/
├── pom.xml (または build.gradle.kts)
├── src/
│   ├── main/
│   │   └── java/
│   │       └── com/
│   │           └── example/
│   │               └── Calculator.java
│   └── test/
│       └── java/
│           └── com/
│               └── example/
│                   └── CalculatorTest.java

テストクラスは、テスト対象クラスと同じパッケージ構造に配置するのが慣例です。これにより、パッケージプライベート(デフォルトアクセス)のメンバーにもアクセスできます。

テストクラスの命名規則

Maven Surefire Pluginは、デフォルトで以下のパターンに一致するクラスをテストクラスとして認識します。

パターン
**/Test*.java TestCalculator.java
**/*Test.java CalculatorTest.java
**/*Tests.java CalculatorTests.java
**/*TestCase.java CalculatorTestCase.java

最も一般的なのは*Test.javaのパターンです。TDDを実践する場合は、このルールに従ってテストクラスを命名してください。

JUnit 5の基本アノテーション

TDDを始めるために最低限知っておくべきアノテーションを紹介します。

@Test

メソッドがテストメソッドであることを示します。JUnit 4と異なり、public修飾子は不要です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import org.junit.jupiter.api.Test;

class CalculatorTest {

    @Test
    void shouldAddTwoNumbers() {
        Calculator calculator = new Calculator();
        int result = calculator.add(2, 3);
        assertEquals(5, result);
    }
}

@DisplayName

テスト結果に表示される名前をカスタマイズします。日本語での記述も可能で、テストの意図を明確に伝えられます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

class CalculatorTest {

    @Test
    @DisplayName("2つの正の整数を加算すると、その合計が返される")
    void shouldAddTwoPositiveNumbers() {
        // ...
    }
}

@BeforeEach / @AfterEach

各テストメソッドの実行前後に呼び出されるセットアップ/クリーンアップメソッドを定義します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

class CalculatorTest {

    private Calculator calculator;

    @BeforeEach
    void setUp() {
        calculator = new Calculator();
    }

    @AfterEach
    void tearDown() {
        // リソースのクリーンアップ(必要な場合)
    }

    @Test
    void shouldAddTwoNumbers() {
        assertEquals(5, calculator.add(2, 3));
    }

    @Test
    void shouldSubtractTwoNumbers() {
        assertEquals(1, calculator.subtract(3, 2));
    }
}

@BeforeAll / @AfterAll

テストクラス内のすべてのテストの実行前後に一度だけ呼び出されます。staticメソッドである必要があります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.AfterAll;

class DatabaseTest {

    @BeforeAll
    static void initDatabase() {
        // データベース接続の初期化
    }

    @AfterAll
    static void closeDatabase() {
        // データベース接続のクローズ
    }
}

@Disabled

テストを一時的に無効化します。スキップされたテストはレポートに表示されます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

class CalculatorTest {

    @Test
    @Disabled("Issue #123 が解決するまで無効化")
    void shouldDivideByZero() {
        // 未実装のテスト
    }
}

@Nested

テストをグループ化し、階層構造を作成します。関連するテストをまとめることで、可読性が向上します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;

@DisplayName("Calculatorクラスのテスト")
class CalculatorTest {

    @Nested
    @DisplayName("addメソッド")
    class AddMethod {

        @Test
        @DisplayName("正の整数同士を加算できる")
        void shouldAddPositiveNumbers() {
            // ...
        }

        @Test
        @DisplayName("負の整数同士を加算できる")
        void shouldAddNegativeNumbers() {
            // ...
        }
    }

    @Nested
    @DisplayName("subtractメソッド")
    class SubtractMethod {
        // ...
    }
}

Assertionsの基本

JUnit 5のアサーションはorg.junit.jupiter.api.Assertionsクラスで提供されます。staticインポートを使用すると、コードがすっきりします。

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

基本的なアサーション

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Test
void basicAssertions() {
    // 等価性の検証
    assertEquals(4, calculator.add(2, 2));
    assertNotEquals(5, calculator.add(2, 2));

    // 真偽値の検証
    assertTrue(calculator.isPositive(5));
    assertFalse(calculator.isPositive(-5));

    // null検証
    assertNotNull(calculator);
    assertNull(calculator.getLastError());

    // 同一性の検証(同じインスタンスか)
    assertSame(expected, actual);
    assertNotSame(expected, actual);
}

失敗メッセージの指定

アサーションが失敗した際に表示されるメッセージを指定できます。メッセージは最後の引数として渡します。

1
2
3
4
5
6
7
8
@Test
void assertionWithMessage() {
    int result = calculator.add(2, 3);
    assertEquals(5, result, "2 + 3 は 5 になるべき");

    // ラムダ式で遅延評価(失敗時のみ評価される)
    assertEquals(5, result, () -> "計算結果が期待値と一致しません: " + result);
}

例外の検証

1
2
3
4
5
6
7
8
9
@Test
void shouldThrowExceptionWhenDivideByZero() {
    ArithmeticException exception = assertThrows(
        ArithmeticException.class,
        () -> calculator.divide(10, 0)
    );

    assertEquals("/ by zero", exception.getMessage());
}

グループアサーション

複数のアサーションをグループ化し、すべての失敗を一度にレポートします。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Test
void groupedAssertions() {
    Person person = new Person("John", "Doe");

    assertAll("person",
        () -> assertEquals("John", person.getFirstName()),
        () -> assertEquals("Doe", person.getLastName()),
        () -> assertNotNull(person.getEmail())
    );
}

タイムアウトの検証

1
2
3
4
5
6
7
@Test
void shouldCompleteWithinTimeout() {
    assertTimeout(Duration.ofSeconds(2), () -> {
        // 2秒以内に完了すべき処理
        service.processData();
    });
}

最初のテストを書いてみる

環境構築が完了したら、TDDの実践としてシンプルな計算機クラスを作成してみましょう。

Redフェーズ: 失敗するテストを書く

まず、src/test/java/com/example/CalculatorTest.javaを作成します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.example;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

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

class CalculatorTest {

    private Calculator calculator;

    @BeforeEach
    void setUp() {
        calculator = new Calculator();
    }

    @Test
    @DisplayName("2つの整数を加算すると合計が返される")
    void shouldAddTwoNumbers() {
        int result = calculator.add(2, 3);
        assertEquals(5, result);
    }
}

この時点でCalculatorクラスは存在しないため、コンパイルエラーになります。これがRedフェーズです。

Greenフェーズ: テストを通す最小限のコードを書く

src/main/java/com/example/Calculator.javaを作成します。

1
2
3
4
5
6
7
8
package com.example;

public class Calculator {

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

テストを実行すると、緑色で成功が表示されます。

1
2
3
mvn test
# または
./gradlew test

Refactorフェーズ: コードを改善する

現時点ではコードがシンプルなので、大きなリファクタリングは不要です。ただし、TDDではこのタイミングでコードの改善を検討します。

このサイクルを繰り返しながら、減算、乗算、除算といった機能を追加していきます。

トラブルシューティング

テストが認識されない

以下の点を確認してください。

  1. テストクラス名が命名規則に従っているか(*Test.javaなど)
  2. @Testアノテーションがインポートされているか(JUnit 4ではなくJUnit 5のorg.junit.jupiter.api.Test
  3. Maven Surefire Plugin または Gradle のuseJUnitPlatform()が設定されているか

「No tests found」エラー

Maven Surefire Plugin のバージョンが古い可能性があります。3.0.0以上にアップグレードしてください。

1
2
3
4
<plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.5.4</version>
</plugin>

IDE でテストが実行できない

プロジェクトの再インポートを試してください。

  • IntelliJ IDEA: File > Invalidate Caches / Restart
  • VS Code: Java: Clean Java Language Server Workspace

まとめ

この記事では、JUnit 5を使ったTDD環境の構築方法を解説しました。

ポイントを振り返ります。

  • JUnit 5は JUnit Platform、JUnit Jupiter、JUnit Vintage の3つで構成される
  • Maven では junit-jupiter 依存関係と Maven Surefire Plugin 3.0以上を設定
  • Gradle では useJUnitPlatform() の指定が必須
  • @Test@BeforeEach@DisplayName が基本アノテーション
  • assertEqualsassertTrueassertThrows が基本的なアサーション

環境が整ったら、次は実際にTDDのRed-Green-Refactorサイクルを回しながらコードを書いてみてください。テストを先に書くという習慣が身につくと、自然と設計品質が向上し、安心してリファクタリングできるようになります。

参考リンク