JUnit5完全ガイド|単体テストの基礎から実務まで完全解説

「テストコードって本当に必要なの?」

Javaでアプリケーション開発を始めたばかりの頃、多くの人が一度はそう感じます。
実際、動くコードを書くだけならテストを書かなくても開発は進みます。

しかし、実務の現場では以下のようなことが起きます。

  • 修正したら別の箇所が壊れた
  • リファクタリングが怖くて手が止まる
  • リリース後にバグが発覚する

こうした問題の多くは、単体テストが整備されていないことが原因です。

そこで登場するのがJUnitです。

JUnitは、Javaで単体テストを書くための標準的なフレームワークです。
現在主流のJUnit5を使えば、メソッド単位でロジックの正しさを自動的に検証できます。

本記事では、JUnit5の基本から実務で通用するレベルまで体系的に解説します。

目次

JUnit5とは?単体テストの本質

JUnitとは?

JUnitは、Java向けの単体テストフレームワークです。

Javaで作成したクラスやメソッドが「期待どおりに動作しているか」を自動で検証する仕組みを提供しています。

現在主流のバージョンはJUnit5です。
新規プロジェクトでは基本的にJUnit5が採用されており、Spring Bootでも標準的に利用されています。

JUnitを使うことで、次のようなことが可能になります。

  • メソッド単位でロジックの正しさを検証
  • 修正後に自動で再確認を実施
  • ビルド時に自動でテストを実行
  • CIと連携して品質を担保

JUnitは単なるライブラリではなく、開発プロセスの品質を支える基盤です。

単体テストとは?

単体テスト(Unit Test)とは、
アプリケーションを構成する最小単位(クラスやメソッド)を個別に検証するテストのことです。

例えば、次のような足し算のメソッドを考えてみます。

// 足し算のメソッド
public int add(int a, int b) {
    // a と b を加算した結果を返す
    return a + b;
}

この add メソッドを以下の観点で確認するのが単体テストです。

  • 正しい結果を返すか
  • 想定外の入力で壊れないか

システム全体ではなく「小さな部品単位」で検証することで、問題の原因を特定しやすくなります。

単体テストと結合テストの違い

単体テストと結合テストは混同しやすいので整理しておきます。

単体テスト

  • メソッドやクラス単位で検証
  • 外部依存(DBやAPI)を基本的に使わない
  • 実行が高速
  • 問題箇所の特定が容易

結合テスト

  • 複数のクラスを組み合わせて検証
  • データベースや外部APIと接続する場合がある
  • 実行時間が長くなることがある
  • 環境に依存しやすい

JUnitは主に単体テストで利用されます。
ロジックの正しさを迅速に保証するためのツールだと理解してください。

なぜJUnitが広く使われているのか

JUnitが事実上の標準となっている理由は明確です。

  • Javaエコシステムとの親和性が高い
  • Maven / Gradleと統合しやすい
  • IDEやCIとの連携が容易
  • Spring Bootと自然に組み合わせられる

特にJUnit5では、以下の改善が行われ、実務で扱いやすくなっています。

  • パラメータ化テストの強化
  • 柔軟なライフサイクル管理
  • 拡張性の向上

JUnitでテストを作成するメリット

JUnitで単体テストを作成するメリットは以下の通りです。

バグの早期発見

小さな単位で検証するため、不具合を素早く見つけることができます。

リファクタリングの安全性

コードを改善しても、テストが通れば動作保証が得られます。
実務ではこれが非常に重要です。

開発効率の向上

テストがあることで以下の観点からも開発スピードが向上します。

  • 手動確認の工数が減る
  • 修正後の不安が減る
  • レビューの説得力が増す

テストを書かないリスク

テストがない状態では、以下のようなリスクがあります。

  • 修正するたびに手動で確認
  • 修正時に別の箇所を壊しても気づかない
  • リリース後に不具合が発覚する可能性が高くなる

最終的に「テストを書かないほうが全体の工数がかかる」という状況に陥る場合もあります。

JUnit5を使うための準備

ここでは、JUnit5で単体テストを実行するための最小限の準備を記載しています。

JUnit5を使うための依存関係

JUnit5を使うには、ビルドツールにテスト用の依存関係を追加します。
※バージョンは執筆時点の安定版を指定していますので、適宜変更してください。

IDEを使っている場合

IntelliJ IDEA / EclipseなどのIDEでは、新規プロジェクト作成時にJUnitが自動で追加されるケースも多く、
意識せずとも使えている場合もあります。その場合は特別な設定は不要です。

Mavenの場合

pom.xml に以下を追加します。

<!-- JUnit5(JUnit Jupiter)の依存関係:テスト時のみ使用する -->
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.10.2</version>
    <!-- scope=test により、本番成果物には含まれない -->
    <scope>test</scope>
</dependency>

Gradleの場合

build.gradle に以下を追加します。

dependencies {
    // JUnit5(JUnit Jupiter)をテスト用依存として追加
    testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
}

test {
    // JUnit5 を実行するための設定(JUnit Platform を使う)
    useJUnitPlatform()
}

useJUnitPlatform() を指定することで、JUnit5が有効になります。

ディレクトリ構造について

標準的なJavaプロジェクトでは、次の構造になっています。

src
 ├── main
 │    └── java
 └── test
      └── java
  • 本番コード → src/main/java
  • テストコード → src/test/java

テストクラスは、本番コードと同じパッケージ構成でsrc/test/java 配下に作成します。

例:

src/main/java/com/example/Calculator.java
src/test/java/com/example/CalculatorTest.java

この構造にしておけば、ビルドツールが自動でテストを検出します。

テストの実行方法

テストを実行する場合はコマンドラインから実行できます。
また、多くのIDEでは、テストクラスを右クリックして実行することが可能になっています。

Maven

# Mavenでテストを実行するコマンド
mvn test

Gradle

# Gradleでテストを実行するコマンド
gradle test

最小構成でJUnitテストを書いてみる

ここでは、シンプルなクラスを例に、JUnitテストの基本構造を説明します。
まずは最小構成で「テストが動く」感覚をつかみましょう。

テスト対象となるクラス

今回は、シンプルな Calculator クラスを例にします。

package com.example;

public class Calculator {

    public int add(int a, int b) {
        // 足し算の結果を返す
        return a + b;
    }
}

テストクラスの作成方法

テストクラスは src/test/java 配下に作成します。

例:

src/main/java/com/example/Calculator.java
src/test/java/com/example/CalculatorTest.java

CalculatorTest クラスを作成します。

package com.example;

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

class CalculatorTest {

    @Test // このメソッドが「テスト」であることを示す
    void add_2と3を渡すと5を返す() {

        // Arrange:テスト対象の準備
        Calculator calculator = new Calculator();

        // Act:対象メソッドを実行
        int result = calculator.add(2, 3);

        // Assert:期待値(5)と実際の値(result)を比較する
        // 一致しなければテストは失敗する
        assertEquals(5, result);
    }
}

ここで重要なのが @Test アノテーションです。
@Test を付けたメソッドが、JUnitによって「テストメソッド」として認識されます。

また、 assertEquals ですが、assertEquals(期待値, 実際の値) の形で記述し、
両者が一致しているかを検証します。

一致しなければ、JUnitはテスト失敗として報告します。

現時点では、assertは「期待どおりの結果になっているかを確認する仕組み」と理解しておけば十分です。

AAAパターンについて

テストコードでは、以下の構造がよく使われます。

  • Arrange(準備)
  • Act(実行)
  • Assert(検証)

今回のコードも、この構造に沿っています。

この形を守ることでテストの意図が明確になり、可読性が上がるメリットがあります。

テストメソッド名の付け方

JUnit5では、テストメソッド名に日本語も使えます。

@Test
void add_負の数同士を足すと正しく計算される() {
    Calculator calculator = new Calculator();
    int result = calculator.add(-2, -3);
    assertEquals(-5, result);
}

重要なのは、どんな条件で何を検証していて、どんな結果を期待しているのかが明確であることです。

テスト名は「仕様を文章で表現したもの」と考えると分かりやすいです。

アサーションについて

ここでは、JUnit5におけるアサーションの役割と代表的なメソッドを説明します。テストの成否はアサーションによって決まるため、正しく理解することが重要です。

アサーションの役割とは?

アサーション(Assertion)とは、「テスト結果が期待した通りになっているか」を検証する仕組みです。
JUnitでは、アサーションが失敗するとテストも失敗します。
アサーションは、テストの「最終判定」を行う部分です。

assertEquals

最も基本的なアサーションです。

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

@Test
void add_2と3を渡すと5を返す() {
    Calculator calculator = new Calculator();

    int result = calculator.add(2, 3);

    // 第1引数:期待値
    // 第2引数:実際の値
    assertEquals(5, result);
}
  • 期待値と実際の値が一致すれば成功
  • 一致しなければ失敗

数値、文字列、オブジェクトなどの比較に使用できます。

assertTrue / assertFalse

真偽値を検証するアサーションです。

import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertFalse;

@Test
void divide_正しく割り算できる場合はtrueになる() {
    boolean result = (10 / 2) == 5;

    // 条件がtrueであることを検証
    assertTrue(result);
}

@Test
void divide_条件が成立しない場合はfalseになる() {
    boolean result = (10 / 2) == 4;

    // 条件がfalseであることを検証
    assertFalse(result);
}
  • 条件式の検証に使う
  • 可読性を意識して使うことが重要

assertNull / assertNotNull

nullチェックに使用します。

import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertNotNull;

@Test
void オブジェクトがnullでないことを確認する() {
    Calculator calculator = new Calculator();

    // nullでないことを検証
    assertNotNull(calculator);
}

@Test
void nullであることを確認する() {
    String value = null;

    // nullであることを検証
    assertNull(value);
}

nullに関するバグは多いため、明示的に検証することが重要です。

assertAll

複数の検証をまとめて実行するためのアサーションです。

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

@Test
void 複数の値をまとめて検証する() {
    Calculator calculator = new Calculator();

    int sum = calculator.add(2, 3);
    int divide = calculator.divide(10, 2);

    // 複数のassertをまとめて実行
    assertAll(
        () -> assertEquals(5, sum),
        () -> assertEquals(5, divide)
    );
}

通常は最初の失敗でテストが止まりますが、assertAll を使うと
すべての検証を実行したうえで結果をまとめて出力します。

メッセージ付きアサーション

失敗時のメッセージを指定できます。

@Test
void 失敗時にメッセージを表示する() {
    int result = 2 + 2;

    // 第3引数にメッセージを指定
    assertEquals(5, result, "計算結果が期待値と一致しません");
}

エラーメッセージを付けておくと原因特定が容易になります。

ライフサイクルと主要アノテーション

ここでは、JUnit5におけるテストの実行順序と、テスト前後に処理を行うためのアノテーションを説明します。
テストが複数になると、共通処理の整理が重要になります。

テストのライフサイクルとは?

JUnitでは、各テストメソッドは基本的に独立して実行されます。

つまり、あるテストの結果が別のテストに影響を与えない設計になっています。

そのため、共通の初期化処理は明示的に記述する必要があります。

@BeforeEach / @AfterEach

@BeforeEach は、各テストメソッドの実行前に毎回呼ばれます。
@AfterEach は、各テストメソッドの実行後に毎回呼ばれます。

例として、簡単なバリデーションクラスを用意します。

package com.example;

public class UserValidator {

    public boolean isValid(String name) {
        // nullまたは空文字の場合は無効
        return name != null && !name.isEmpty();
    }
}

テストコードは次のようになります。

package com.example;

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

import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertFalse;

class UserValidatorTest {

    private UserValidator validator;

    @BeforeEach
    void setUp() {
        // 各テスト実行前に新しいインスタンスを生成
        validator = new UserValidator();
    }

    @Test
    void 名前が空でなければ有効() {
        // trueになることを検証
        assertTrue(validator.isValid("Taro"));
    }

    @Test
    void 名前が空文字なら無効() {
        // falseになることを検証
        assertFalse(validator.isValid(""));
    }
}

このように共通処理をまとめることで、テストコードが整理されます。

@BeforeAll / @AfterAll

@BeforeAll は、テストクラス全体で最初に一度だけ実行されます。
@AfterAll は、テストクラス全体で最後に一度だけ実行されます。

注意点として、これらは static メソッドで定義する必要があります。

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.AfterAll;

class SampleTest {

    @BeforeAll
    static void initAll() {
        // テストクラス全体の初期化処理
        System.out.println("全テスト開始前に一度だけ実行");
    }

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

データベース接続の準備などで利用されることがあります。

@DisplayName

テストに分かりやすい表示名を付けることができます。

import org.junit.jupiter.api.DisplayName;

@Test
@DisplayName("名前が空文字の場合は無効になる")
void testEmptyName() {
    assertFalse(validator.isValid(""));
}

実行結果の表示が読みやすくなります。

@Nested

テストを論理的にグループ化できます。

import org.junit.jupiter.api.Nested;

class UserValidatorTest {

    @Nested
    class 正常系 {

        @Test
        void 有効な名前() {
            assertTrue(new UserValidator().isValid("Taro"));
        }
    }

    @Nested
    class 異常系 {

        @Test
        void 空文字は無効() {
            assertFalse(new UserValidator().isValid(""));
        }
    }
}

テスト構造を整理したい場合に有効です。

@Disabled

一時的にテストを無効化できます。

import org.junit.jupiter.api.Disabled;

@Test
@Disabled("未実装のため一時的に無効化")
void 未実装のテスト() {
}

ただし、長期間放置するのは推奨されません。

例外テストの書き方

ここでは、例外が発生するケースをどのようにテストするかを説明します。
アプリケーションは「正常に動くこと」だけでなく、「正しく失敗すること」も重要です。

例外をテストする必要性

これまでのテストは、正しい値が返ることを検証してきました。

しかし、実際のアプリケーションでは、不正な入力、想定外の値、禁止された操作などによって例外が発生します。

その例外が「正しく発生するか」も単体テストで確認する必要があります。

assertThrowsの使い方

例として、以下の divideメソッドを使います。

public int divide(int a, int b) {
    // bが0の場合はArithmeticExceptionが発生する
    return a / b;
}

0で割ると ArithmeticException が発生します。

この例外をテストするには assertThrows を使用します。

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

class CalculatorTest {

    @Test
    void divide_0で割るとArithmeticExceptionが発生する() {

        Calculator calculator = new Calculator();

        // 指定した例外が発生することを検証する
        // 第1引数:期待する例外クラス
        // 第2引数:例外が発生する処理(ラムダ式)
        assertThrows(ArithmeticException.class, () -> {
            calculator.divide(10, 0);
        });
    }
}

assertThrows は、

  • 指定した例外が発生すれば成功
  • 発生しなければ失敗

となります。

例外メッセージの検証

例外オブジェクトを取得することもできます。

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

@Test
void divide_例外メッセージを確認する() {

    Calculator calculator = new Calculator();

    // 発生した例外を受け取る
    ArithmeticException exception =
        assertThrows(ArithmeticException.class, () -> {
            calculator.divide(10, 0);
        });

    // 例外メッセージを検証
    assertEquals("/ by zero", exception.getMessage());
}

業務ロジックで独自例外を投げる場合、
メッセージ内容まで検証することが重要になります。

やってはいけない例外テスト

try-catchで例外を握りつぶす方法は推奨されません。

@Test
void NGな例外テスト() {

    Calculator calculator = new Calculator();

    try {
        calculator.divide(10, 0);
    } catch (ArithmeticException e) {
        // 何も検証していないためテストとして不十分
    }
}

この書き方では、例外が発生しなくてもテストが成功してしまい検証が曖昧になってしまいます。

例外テストでは、必ず assertThrows を使用します。

例外テストは、正常系と同じくらい重要です。
「失敗するべき場面で正しく失敗するか」を確認することが、堅牢なコードにつながります。

パラメータ化テスト

ここでは、同じロジックに対して複数の入力パターンを効率よく検証する方法を説明します。
テストケースが増えると、コードの重複を避ける工夫が重要になります。

パラメータ化テストの目的

例えば、add メソッドをさまざまな値で検証したい場合を考えます。

  • 2 + 3 = 5
  • -1 + 1 = 0
  • 0 + 0 = 0

これらを個別に @Test で書くこともできますが、
構造が同じテストが増えていきます。

パラメータ化テストを使えば、1つのテストメソッドで複数パターンを検証できます。

@ParameterizedTest / @ValueSource

引数が1つの場合は@ParameterizedTest@ValueSourceで複数パターンの検証ができます。

@ValueSourceは単一引数のテストに対して、値を配列で渡せます。数値や文字列などを簡単に列挙できます。

その他のよく使う型のために、Stringからの暗黙的な型変換がサポートされています。

※サポートの詳細は JUnit公式ドキュメント をご確認ください。

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

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

class PositiveNumberTest {

    @ParameterizedTest // パラメータ化テストであることを示す
    @ValueSource(ints = {1, 2, 3, 10}) // テストに渡す値の一覧
    void 正の数は0より大きい(int number) {

        // 各値が順番に number に渡される
        assertTrue(number > 0);
    }
}

@ValueSource で指定した値が、順番に引数へ渡されます。

@CsvSourceを使った複数引数のテスト

複数の引数を扱う場合は @CsvSource を使用します。

import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;

class CalculatorParameterizedTest {

    @ParameterizedTest
    @CsvSource({
        "2, 3, 5",
        "-1, 1, 0",
        "0, 0, 0"
    })
    void add_複数パターンを検証(int a, int b, int expected) {

        Calculator calculator = new Calculator();

        // a + b の結果が expected と一致するか検証
        assertEquals(expected, calculator.add(a, b));
    }
}

各行ごとに1つのテストケースを指定し、カンマ区切りで記述します。

@MethodSourceによる柔軟なテストデータ供給

より複雑なデータを使う場合は @MethodSource を使用します。

import org.junit.jupiter.params.provider.MethodSource;
import java.util.stream.Stream;
import org.junit.jupiter.params.provider.Arguments;

class CalculatorMethodSourceTest {

    @ParameterizedTest
    @MethodSource("provideAddCases")
    void add_様々な値を検証(int a, int b, int expected) {

        Calculator calculator = new Calculator();

        assertEquals(expected, calculator.add(a, b));
    }

    // テストデータを提供するメソッド
    static Stream<Arguments> provideAddCases() {
        return Stream.of(
            Arguments.of(2, 3, 5),
            Arguments.of(-2, -3, -5),
            Arguments.of(100, 200, 300)
        );
    }
}

@MethodSource を使うと、複雑なオブジェクトや条件分岐を含むデータ、大量のケースに対応できます。

どんな場面で使うべきか

パラメータ化テストは以下のような場面で有効です。

  • 入力値のバリエーションが多い
  • 境界値を複数検証する
  • 同じロジックを繰り返し検証する

重複したテストコードを減らし、保守性を高めるための仕組みです。

パラメータ化テストを使うことで、テストの網羅性と可読性を両立できます。

テスト設計の基本

ここでは、JUnitの書き方ではなく「何をテストするべきか」という設計の考え方を説明します。
テストコードの質は、アサーションの書き方よりも、テストケースの設計で決まります。

正常系 / 異常系の整理

まず意識すべきなのは、正常系 / 異常系の切り分けです。

例として以下のdivideメソッドで考えます。

public int divide(int a, int b) {
    // bが0の場合はArithmeticExceptionが発生する
    return a / b;
}

このメソッドに対して考えられる入力は

  • 正常系:10 / 2 = 5
  • 異常系:10 / 0 → ArithmeticException

テスト設計では、正しく動くこと正しく失敗することの両方を網羅します。

@Test
void divide_通常の割り算が成功する() {
    Calculator calculator = new Calculator();

    // 正常系の検証
    assertEquals(5, calculator.divide(10, 2));
}

@Test
void divide_0で割ると例外が発生する() {
    Calculator calculator = new Calculator();

    // 異常系の検証
    assertThrows(ArithmeticException.class,
            () -> calculator.divide(10, 0));
}

境界値分析

バグは「端の値」で発生しやすいという特徴があります。

例えば、年齢チェックのロジックを考えます。

public boolean isAdult(int age) {
    // 20歳以上を成人とする
    return age >= 20;
}

この場合、テストすべき値は以下の通りです。

  • 19(境界の直前)
  • 20(境界値)
  • 21(境界の直後)
@Test
void isAdult_境界値を検証する() {

    assertFalse(isAdult(19)); // 境界直前
    assertTrue(isAdult(20));  // 境界値
    assertTrue(isAdult(21));  // 境界直後
}

同値分割

入力値を「同じ振る舞いになるグループ」に分けて考える方法です。

例えば年齢の分類を考えると、19歳以下が未成年、20歳以上を成人とします。

0歳から100歳までの全てでテストをするのは非効率なため、
以下のように代表を決定し、無駄なテストを減らしつつ網羅性を保ちます。

  • 10(未成年グループ代表)
  • 30(成人グループ代表)

テストケースの洗い出し方

実務では、次の順で考えると整理しやすくなります。

  1. メソッドの仕様を言語化する
  2. 入力パターンを洗い出す
  3. 正常系 / 異常系に分類する
  4. 境界値を探す

テストは「思いつき」で書くのではなく、仕様から逆算します。

カバレッジの考え方

カバレッジとは、「コードのどれだけがテストで実行されたか」を示す指標です。

ただし、注意すべき点があります。

  • カバレッジ100%でもバグは存在する
  • 数字だけを追うと意味のないテストが増える

重要なのは、仕様を満たしているかどうかであり、カバレッジは補助指標に過ぎません。
テスト設計を意識することで、JUnitは単なるツールではなく、設計力を高める武器になります。

MockとMockitoの基礎

ここでは、単体テストにおける「依存の切り離し」Mockの考え方を説明します。
JUnit単体では解決できない場面を理解することが目的です。

依存を持つクラスの問題

これまで扱ってきた CalculatorUserValidator は、外部に依存していませんでした。

しかし実際のアプリケーションでは、次のようなクラスを使用することがあります。

package com.example;

public class PaymentGateway {

    public boolean charge(int amount) {
        // 外部決済APIを呼び出す想定
        return true;
    }
}
package com.example;

public class OrderService {

    private final PaymentGateway paymentGateway;

    // 外部依存をコンストラクタで受け取る
    public OrderService(PaymentGateway paymentGateway) {
        this.paymentGateway = paymentGateway;
    }

    public boolean placeOrder(int amount) {
        // 決済処理が成功したら注文成功
        return paymentGateway.charge(amount);
    }
}

OrderServicePaymentGateway に依存しています。

このままテストを行うと以下のような問題が発生してしまいます。

  • 実際の外部APIを呼び出してしまう
  • 実行速度が遅くなる
  • テストが不安定になる

これらを解決するためにMockを使用します。

Mockとは?

Mockとは、本物の代わりに振る舞う偽物のオブジェクトです。

テストでは、外部APIやデータベース、メール送信処理などをMockに置き換えることで、
「テスト対象のロジックだけ」を検証します。

Mockitoを使った基本例

Mockitoは、Javaで広く使われているMockライブラリです。

依存関係(Maven例)

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>5.11.0</version>
    <scope>test</scope>
</dependency>

依存関係(Gradle例)

dependencies {
    // Mockito本体(Mockを作成するためのライブラリ)
    testImplementation 'org.mockito:mockito-core:5.11.0'

    // JUnit5とMockitoを連携させる拡張(@ExtendWith(MockitoExtension.class) を使う場合に必要)
    testImplementation 'org.mockito:mockito-junit-jupiter:5.11.0'
}

テストコード例:

import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when;

class OrderServiceTest {

    @Test
    void 決済成功時は注文成功となる() {

        // PaymentGatewayのMockを作成
        PaymentGateway mockGateway = Mockito.mock(PaymentGateway.class);

        // charge(1000) が呼ばれたら true を返すように設定
        when(mockGateway.charge(1000)).thenReturn(true);

        // Mockを注入してサービスを作成
        OrderService service = new OrderService(mockGateway);

        // 実行
        boolean result = service.placeOrder(1000);

        // 検証
        assertTrue(result);
    }
}

ここで重要なのは、Mockは初期状態だとデフォルト値を返します。
期待する戻り値が必要な場合は when(...).thenReturn(...) で振る舞いを定義します。

どこまで使うべきか

単体テストでは以下のような場合にMockを使用します。

  • 外部と通信する部分
  • 結果が環境に依存する処理
  • 実行に時間がかかる処理

ただし、Mockを使いすぎると実際の動作と乖離してテストが複雑になる問題もあります。

重要なのはテスト対象のロジックを検証するために必要な範囲だけMockするという姿勢です。

Mockを理解すると、単体テストの適用範囲が大きく広がります。

実務におけるJUnitの位置づけ

ここでは、JUnitが実務の開発フローの中でどう使われるかを説明します。
学習目的で終わらず、チーム開発の品質を支える仕組みとして捉えることがポイントです。

CIとの関係

実務では、JUnitは個人のローカル確認だけで完結しないことが多いです。
また、CI(継続的インテグレーション)で、プルリクエストやpush時にテストを自動実行します。

これにより、テストが通らない変更が混入しにくくなり、品質を一定以上に保てます。

実務でよくある流れは以下の通りです。

  • 開発者が変更をコミットしてプルリクエストを作成する
  • CIが自動的にビルドとJUnitテストを実行する
  • テストがすべて成功した場合のみマージできる運用にする

この運用では、JUnitは「任意の確認」ではなく「品質ゲート」として扱われます。

ビルドとテストの統合

Maven / Gradle を使うプロジェクトでは、テストはビルド工程と統合されます。つまり、ビルドを通すためにはテストが通る必要がある、という前提が置かれます。これが「テストが通らないコードはリリースできない」という仕組みを技術的に支えています。

代表的な実行コマンドは以下の通りです。

  • Maven:mvn test
  • Gradle:gradle test

CI上でも同様のコマンドが実行されることが多く、JUnitテストは日常的に回り続けます。

Spring Bootとの関係

Spring Bootプロジェクトでは、JUnit5が標準で組み込まれていることが多く、追加の設定なしでテストを書き始められるケースもあります。また、Spring Bootにはアプリケーションコンテキストを起動して検証する仕組みが用意されており、代表例として @SpringBootTest があります。

ただし、単体テストと結合テストは目的が違うため、使い分けが重要です。ポイントは以下の通りです。

  • ロジック単体を高速に検証したい場合は、JUnitによる単体テストを中心にする
  • DIやDB接続など、複数要素を含めた動作確認をしたい場合は、Springのテスト機能を使う

単体テストは速さと原因特定のしやすさが強みで、結合寄りのテストは信頼性の確認が強みです。
両者を混同しないことで、テスト運用が破綻しにくくなります。

テストが評価される理由

実務で「テストが書けること」が評価につながるのは、単に品質意識が高いからではありません。
テストを書こうとすると、設計の歪みが露呈しやすいからです。

依存が密結合で差し替えできなかったり、メソッドの責務が大きすぎたりすると、単体テストを書きにくくなります。

テストが書ける状態のコードは、設計が整理されていることが多いです。
つまり、JUnitを使いこなすことは、単体テストのスキルだけでなく、設計力を示すことにもつながります。

まとめ

ここでは、本記事で扱った内容を整理し、JUnit5を活かすためのポイントを簡潔に振り返ります。

まず身につけるべきこと

本記事で扱った内容のうち、最初に確実に使えるようにすべきポイントは以下の通りです。

  • @Test を使って基本的な単体テストを書けること
  • assertEquals / assertTrue / assertThrows を適切に使い分けられること
  • 正常系 / 異常系を分けてテストケースを設計できること
  • 外部依存がある場合にMockを使う発想を持てること

これらはすべて、実務で求められる基礎力です。
これらが自然に書けるようになれば、実務で最低限困らない状態になります。

テストは品質保証だけではない

単体テストは「バグを防ぐための仕組み」という側面があります。
しかしそれ以上に重要なのは、設計を健全に保つ役割です。

テストを書こうとすると、次のような問題に気づきやすくなります。

  • 依存関係が密になっていないか
  • メソッドの責務が大きすぎないか
  • 副作用が多すぎないか

テストが書きにくいコードは、多くの場合、設計にも問題があります。
JUnitを使うことは、設計力を磨くことにも直結します。

今後の学習の広げ方

JUnit5の基礎を理解したら、次の領域に広げていくと実務での強みになります。

  • Mockitoの活用範囲を広げる
  • Spring Bootのテスト機能(@SpringBootTest など)
  • テスト駆動開発(TDD)の実践
  • カバレッジツールの活用

これらはすぐにすべて理解する必要はありません。
重要なのは、JUnitを中心に据えたうえで段階的に広げていくことです。

テストが書けるエンジニアの価値

テストが書けるエンジニアは、単にバグを減らせる人ではありません。
コードの責務を整理し、依存を分離し、変更に強い構造を作れる人です。

JUnitを使いこなせるということは、仕様を理解し、それを検証可能な形に落とし込めるということでもあります。
これは設計力の一部です。

本記事で扱った内容を実際のコードに適用し、テストを書く習慣を身につけることが、
設計力を高めるための基盤になります。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次