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

Java入門

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

トム

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

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

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

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

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

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

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

JUnitとは何か?

JUnitは、Javaプログラムの品質を保証するための「ユニットテスト(単体テスト)」を記述し、実行するためのフレームワークです。Javaにおけるテストのデファクトスタンダードであり、多くの開発現場で利用されています。

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

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

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

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

メリット

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

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

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

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

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

JUnit5は、JUnit PlatformJUnit JupiterJUnit Vintageの3つのモジュールで構成されています。このモダンな設計により、拡張性が大幅に向上しました。これから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)を使っている場合、さらに簡単です。

MavenやGradleプロジェクトを新規作成する際に、ウィザード画面で「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)」、つまり検証です。メソッドの実行結果が期待通りであるかを確認します。アサーションにはorg.junit.jupiter.api.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引数に例外をスローするはずの処理をラムダ式で渡します。これにより、意図した例外が正しく発生するかを安全にテストできます。

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

読みやすく、保守しやすいテストコードを書くためには、構造と設計が重要になります。ここでは、優れたテストコードを書くための基本的なパターンと命名規則を紹介します。

Arrange-Act-Assertパターンとは

テストコードはArrange-Act-Assert (AAA) パターンで記述するのが最も効果的です。 このパターンに従うことで、テストの目的が明確になり、誰が見ても分かりやすいコードになります。

AAAパターン

  • 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が主戦場とする「単体テスト(ユニットテスト)」は、クラスやメソッドなど、単一のコンポーネントを対象とします。外部のデータベースやAPIなどへの依存を排除し、対象のロジックのみを検証するのが特徴です。

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

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

JUnitの便利な機能

JUnit5には、基本的なテスト記述をさらに効率化し、表現力豊かにするためのアノテーションが多数用意されています。

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

複数のテストメソッドで共通のオブジェクト生成や初期化処理が必要な場合、@BeforeEachアノテーションが役立ちます。このアノテーションが付いたメソッドは、各@Testメソッドが実行される前に必ず一度実行されます。

同様に、@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で最も人気のあるモックライブラリの一つです。これを使うことで、依存オブジェクトの振る舞いを自由に定義し、テスト対象のロジックだけに集中できます。

導入は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のロジックをテストできました。

まとめ

今回は、JavaのユニットテストフレームワークであるJUnit5の基本について、導入方法から基本的な書き方、モックを使った応用的なテストまでを網羅的に解説しました。

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

その理由は、JUnitがもたらすメリットが非常に大きいからです。

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

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

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

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

トム

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

-Java入門