本ページはプロモーションが含まれています

Java入門

Javaのユニットテスト(単体テスト)が変わる!JUnit5の基本を徹底解説

トム

・都内自社開発企業勤務/Javaバックエンドエンジニア
/Java歴10年以上 ・首都圏在住30代
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎 詳細なプロフィール

「Javaで開発しているけど、テストコードの書き方がいまいちわからない…」

「品質の高いコードを書きたいけど、ユニットテストって何から始めればいいの?」

私が新人だった頃、まさに同じ悩みを抱えていました。当時はテストコードの重要性を理解しつつも、書き方がわからず苦労した経験があります。

JUnitによるユニットテストを本格的に導入してから、開発体験が変わりました。 自信を持ってコードを修正できるようになったのです。後輩に指導する機会も増え、つまずきやすいポイントがわかってきました。

この記事は、過去の私や後輩たちのような「Javaのユニットテストについて知りたい方」に向けて書きました。

この記事を読めば、Javaにおけるユニットテストの標準フレームワークであるJUnitの基本を完全に理解し、明日からテストコードを書き始められます。

この記事でわかること

  • JUnit5の導入方法(Maven/Gradle)
  • @Test・アサーション・例外テストの基本的な書き方
  • Arrange-Act-Assertパターンによるテスト設計
  • @BeforeEach・@ParameterizedTest・@Nestedなどの便利機能
  • Mockitoを使ったモックテストの基礎

JUnitとは何か?

JUnitは、Javaのユニットテスト(単体テスト)を書いて実行するためのフレームワークです。Javaのテストツールとして最も広く使われています。

JUnitは高品質なソフトウェア開発に欠かせません。 なぜなら、コードが意図通りに動くかを確認する作業を自動化し、バグの早期発見や安全なリファクタリング(コードの改善作業)を力強く支援してくれるからです。

ユニットテストの目的と重要性

ユニットテストの目的は、クラスやメソッドといったプログラムの最小単位(ユニット)が、それぞれ正しく機能するかを検証することにあります。

ユニットテストを導入するメリットは主に3つです。

メリット

  1. 品質の向上: バグを開発の早い段階で発見し、修正できます。
  2. リファクタリングの促進: テストコードがあれば、コードを修正した際に意図しない不具合(デグレード)が発生していないかを即座に確認できます。
  3. 仕様のドキュメント化: よく書かれたテストコードは、そのコードが「どのように動くべきか」を示す生きたドキュメントになります。

ユニットテストはJava開発において非常に重要な役割を担っています。

JUnitの概要とバージョンの違い(JUnit4とJUnit5)

JUnitにはいくつかのバージョンがありますが、現在主流なのはJUnit5です。以前はJUnit4が広く使われていました。

JUnit4とJUnit5には、アーキテクチャやアノテーションの面でいくつかの違いがあります。例えば、テスト前の初期化処理は、JUnit4では@Beforeでしたが、JUnit5では@BeforeEachに変わりました。

JUnit5は3つのモジュール(JUnit PlatformJUnit JupiterJUnit Vintage)で構成されています。拡張性が大幅に向上しました。これからJavaのテストを学ぶなら、JUnit5から始めましょう。

JUnitの導入方法

JUnitをプロジェクトに導入するのは非常に簡単です。特に、MavenやGradleといったビルドツールを利用すると、数行の設定を追加するだけで済みます。

Maven/Gradleによる導入

最近のJavaプロジェクトでは、依存ライブラリの管理にMavenまたはGradleを使用するのが一般的です。

Mavenの場合 (pom.xml)

pom.xmlファイルの<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>

<scope>test</scope>は、このライブラリがテストコードのコンパイルと実行時にのみ必要であることを示します。

Gradleの場合 (build.gradle)

build.gradleファイルに以下を記述します。

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

test {
    useJUnitPlatform()
}

IDE(IntelliJ/Eclipse)でのセットアップ

IntelliJ IDEAやEclipseなどの統合開発環境(IDE)を使っている場合、さらに簡単です。

プロジェクト作成時のウィザードで「JUnit5」を選ぶだけで、必要な設定は自動で完了します。テストクラスを手動で作る場合も、IDEがJUnitの追加を提案してくれます。

基本的なテストの書き方

JUnitの導入が完了したら、いよいよテストコードを書いていきましょう。ここでは、計算を行うシンプルなCalculatorクラスを例に、基本的なテストの書き方を解説します。

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

@Testアノテーションの使い方

JUnit5では、テストしたいメソッドに@Testアノテーションを付けることで、そのメソッドがテストケースであることをフレームワークに伝えます。

テストクラスは、慣習としてsrc/test/javaディレクトリ配下に作成します。

import org.junit.jupiter.api.Test;

class CalculatorTest {
    @Test
    void testAdd() {
        // ここにテストのロジックを記述する
    }
}

アサーション(assertEquals / assertTrueなど)

テストコードの核心は「アサーション(Assertion)」です。メソッドの実行結果が期待どおりかを検証します。Assertionsクラスのstaticメソッドを使います。

最もよく使われるのが、2つの値が等しいかを確認するassertEqualsです。

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

class CalculatorTest {
    @Test
    void 足し算が正しくできるか検証する() {
        Calculator calculator = new Calculator();
        int result = calculator.add(2, 3);
        assertEquals(5, result, "2 + 3 は 5 になるべきです");
    }
}

assertEqualsは、第1引数に「期待値」、第2引数に「実際の値」を取ります。第3引数は、テストが失敗したときに表示されるメッセージで、省略も可能です。

他にも、以下のような便利なアサーションメソッドがあります。

アサーションメソッド

  • assertTrue(boolean condition): 条件がtrueであることを検証
  • assertFalse(boolean condition): 条件がfalseであることを検証
  • assertNotNull(Object obj): オブジェクトがnullでないことを検証
  • assertNull(Object obj): オブジェクトがnullであることを検証

例外の検証

特定の条件下で、メソッドが正しく例外をスローするかを検証したいケースもあります。JUnit5では、assertThrowsメソッドを使って簡単に例外テストを記述できます。

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

class SomeServiceTest {
    @Test
    void 引数がnullの場合に例外をスローするか() {
        SomeService service = new SomeService();
        // service.process(null) を実行した際に IllegalArgumentException がスローされることを期待する
        assertThrows(IllegalArgumentException.class, () -> {
            service.process(null);
        });
    }
}

assertThrowsの第1引数には期待する例外の型、第2引数には例外が出るはずの処理をラムダ式で渡します。意図した例外が正しく発生するかを安全にテストできます。

テストケースの構造と設計

読みやすく保守しやすいテストを書くには、構造と命名が大切です。3つのポイントを紹介します。

Arrange-Act-Assertパターンとは

テストコードはArrange-Act-Assert (AAA) パターンで書くのが効果的です。 テストの目的が明確になり、誰が見てもわかりやすくなります。

AAAパターン

  • Arrange(準備): テスト対象のオブジェクトを生成し、必要なデータや状態を準備するフェーズ。
  • Act(実行): テスト対象のメソッドを実行し、結果を受け取るフェーズ。
  • Assert(検証): 実行結果が期待通りであるかをアサーションメソッドで検証するフェーズ。

Arrange・Act・Assertの3フェーズをコメントや空行で明確に分けることで、テストコードの読みやすさは飛躍的に向上します。

@Test
void 足し算が正しくできるか検証する_AAAパターン() {
    // Arrange
    Calculator calculator = new Calculator();
    int a = 10;
    int b = 20;
    int expected = 30;

    // Act
    int actual = calculator.add(a, b);

    // Assert
    assertEquals(expected, actual);
}

テストクラスとテストメソッドの命名規則

命名は非常に重要です。後から見返したときに、そのテストが「何を」「どのような状況で」「どう検証しているか」が一目でわかる名前にするのが理想です。

  • テストクラス名: [テスト対象クラス名]Testとするのが一般的です。(例: CalculatorTest
  • テストメソッド名: 英語ならshould[期待する結果]When[条件]のような形式があります。日本語なら「〇〇の場合は〇〇を返すこと」のように自然な文章で書くとわかりやすいです。

良い命名は、テストが失敗したときのエラーメッセージを読むだけで、問題の原因を推測する手助けとなります。

ユニットテスト(単体テスト)と統合テストの違い

JUnitが得意とする「ユニットテスト(単体テスト)」は、クラスやメソッドなど1つの部品を対象にします。データベースやAPIなど外部への依存を切り離し、対象のロジックだけを検証します。

一方、「統合テスト」は、複数のコンポーネントを組み合わせて、連携がうまくいくかを確認するテストです。例えば、「ControllerからServiceを呼び出し、Repository経由でデータベースにアクセスする」といった一連の流れをテストします。

まずはユニットテストをしっかり書くことが、品質保証の第一歩です。

JUnitの便利な機能

基本をマスターしたら、テストをさらに効率化するアノテーションを使いましょう。

@BeforeEach / @AfterEach(初期化と後処理)

@BeforeEachは、各テストの前に共通の初期化処理を実行するアノテーションです。複数のテストで同じオブジェクト生成が必要なときに役立ちます。

同様に、@AfterEachは各テストメソッドの実行に呼ばれ、リソースの解放などの後処理に使います。

class CalculatorTest {

    private Calculator calculator;

    @BeforeEach
    void setUp() {
        // 各テストの前にCalculatorインスタンスを生成する
        this.calculator = new Calculator();
        System.out.println("テスト準備完了");
    }

    @AfterEach
    void tearDown() {
        // 各テストの後に実行される
        System.out.println("テスト後処理完了");
    }

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

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

@ParameterizedTestによるパラメータ化テスト

同じロジックのテストを、異なる入力値と期待値で何度も実行したい場合があります。@ParameterizedTestを使うと、テストコードの重複を避けられます。

@ValueSourceを使えば、簡単な値のリストをパラメータとして渡せます。

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

class StringUtilsTest {
    @ParameterizedTest
    @ValueSource(strings = {"", "  ", "\t"})
    void 文字列がブランクであるか検証する(String input) {
        assertTrue(input.isBlank());
    }
}

さらに、@CsvSourceを使えば、カンマ区切りの値で複数の引数(入力値と期待値など)を渡すことも可能です。

@Nestedによる階層的テスト構造

テスト対象クラスの機能が複雑になると、テストクラスも肥大化しがちです。@Nestedアノテーションを使い、内部クラスでテストをグループ化すると、テストの構造がわかりやすくなります。

class BankAccountTest {

    // 口座の初期状態に関するテスト
    @Nested
    class 初期状態のテスト {
        @Test
        void 新規口座の残高は0円であること() {
            // ...
        }
    }

    // 入金機能に関するテスト
    @Nested
    class 入金テスト {
        @Test
        void 正の金額を入金すると残高が増えること() {
            // ...
        }

        @Test
        void 負の金額を入金しようとすると例外が発生すること() {
            // ...
        }
    }
}

関連するテストをまとめることで、読みやすさが格段に向上します。

モックを使ったテスト

実際の開発では、クラスが他のクラスや外部システム(データベース、APIなど)に依存していることがほとんどです。依存先まで含めるとユニットテストの範囲を超えてしまいます。

そこで登場するのが「モック(Mock)」です。モックとは、本物のオブジェクトのふりをする偽物のオブジェクトのこと。Mockitoなどのモックライブラリを使うと、モックを簡単に作成できます。

Mockitoの基本と導入方法

Mockitoは、Javaで最も人気のあるモックライブラリの一つです。Mockitoを使うことで、依存オブジェクトの振る舞いを自由に定義し、テスト対象のロジックだけに集中できます。

導入はJUnitと同様に、ビルドツールに依存関係を追加するだけです。

Mavenの場合 (pom.xml)

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>5.12.0</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <version>5.12.0</version>
    <scope>test</scope>
</dependency>

依存オブジェクトのモック化と検証

例えば、ユーザー情報をUserRepositoryから取得して処理を行うUserServiceをテストしたいとします。

// テスト対象クラス
public class UserService {
    private final UserRepository repository;

    public UserService(UserRepository repository) {
        this.repository = repository;
    }

    public String getUserName(int id) {
        User user = repository.findById(id);
        if (user != null) {
            return user.getName();
        }
        return "Unknown";
    }
}

UserRepositoryはデータベースにアクセスするため、ユニットテストではモックに置き換えます。Mockitoを使えば、次のようにテストを書けます。

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class) // Mockito拡張を有効にする
class UserServiceTest {

    @Mock // UserRepositoryのモックを作成
    private UserRepository mockRepository;

    @InjectMocks // モックを注入してUserServiceのインスタンスを生成
    private UserService userService;

    @Test
    void 存在するユーザーIDの場合はユーザー名を返すこと() {
        // Arrange: モックの振る舞いを定義
        User user = new User(1, "Taro");
        // repository.findById(1)が呼ばれたら、作成したuserオブジェクトを返すように設定
        when(mockRepository.findById(1)).thenReturn(user);

        // Act: テスト対象メソッドを実行
        String userName = userService.getUserName(1);

        // Assert: 結果を検証
        assertEquals("Taro", userName);

        // Verify: repository.findById(1)が1回だけ呼ばれたことを検証
        verify(mockRepository, times(1)).findById(1);
    }
}

when(...).thenReturn(...)でモックの振る舞い(スタブ:あらかじめ決めた値を返す設定)を定義し、verify(...)でモックのメソッドが期待通りに呼ばれたかを検証します。モックを使うことで、データベースに接続せずにUserServiceのロジックをテストできました。

まとめ

JUnitを習得し、日々の開発にユニットテストを取り入れることは、プロのJavaエンジニアとして成長するために避けては通れない道です。

JUnitを活用することで得られるメリット

  • コードの品質が向上する: 自動化されたテストが、バグの混入を未然に防ぎます。
  • 開発速度が上がる: 手動テストの時間を削減し、問題の早期発見により手戻りを減らせます。
  • 自信を持って開発できる: テストという安全網があるため、リファクタリングや機能追加を恐れずに行えます。
  • 仕様が明確になる: テストコードが、コードの振る舞いを定義するドキュメントとして機能します。

最初はテストコードに時間がかかります。しかし、長期的に見れば、その投資は保守性の高い、堅牢なアプリケーションという形で何倍にもなって返ってきます。

JUnit5の基本を理解したら、次はSpring Bootでの統合テストや、カバレッジ計測ツール(JaCoCo)に挑戦してみてください。

  • この記事を書いた人
  • 最新記事

トム

・都内自社開発企業勤務/Javaバックエンドエンジニア
/Java歴10年以上 ・首都圏在住30代
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎 詳細なプロフィール

-Java入門