📑

「UT(単体テスト)って何?」から始める、JUnit 5 & Mockito 入門~実践

に公開

はじめに

バグ修正コストは「要件定義時を 1 とすると運用時には 100 倍」とも言われる。
単体テスト(Unit Test)は、クラスや関数など最小単位の振る舞いを検証する工程だ。テストピラミッドで最下層に位置し、数と実行速度を武器にプロダクト全体の品質を支える。
「仕様をあとから変更できる安心感」──これこそ単体テストが開発者にもたらす最大の価値である。
本記事は JUnit 5 と Mockito を軸に、環境構築から実践的な応用技法まで解説するハンズオンガイドである。

UT(単体テスト)とは

単体テスト(Unit Test) は、ソフトウェア開発において、プログラムの最小単位(関数、メソッド、クラスなど)が期待通りに動作するかを検証するテストのこと。

【主な特徴】

  1. テスト対象
    個別の関数やメソッド
    単一のクラス
    小さなモジュール

  2. 独立性
    他のコンポーネントに依存しない
    データベースやファイルシステムなどの外部リソースを使わない
    モックやスタブを使用して依存関係を排除

  3. 自動化
    自動実行可能
    継続的インテグレーション(CI)に組み込める
    素早くフィードバックを得られる

UT(単体テスト)の対象範囲

UT の対象は「単一ユニット/モジュール」であり、外部コンポーネントとは切り離して検証する。
• カバーすべきは内部ロジック・インタフェース・状態管理・異常系で、必要に応じてコードカバレッジ指標を測定する。
• ユニット間結合やシステム全体の特性は UT の対象外で、上位レベル(結合テスト・システムテスト)で扱う。

テスト対象の粒度

• 「ユニット(unit)」または「モジュール(module)」と呼ばれる最小レベルのソフトウェア構成要素。
– 関数/メソッド/クラス
– ライブラリや DLL に相当する小規模コンポーネント
– AUTOSAR SW-C や RTOS タスクなど、開発組織が「最小テスト単位」と定義した要素
• 隣接ユニットとの直接結合部(公開 API、入力ポートなど)はモックやスタブで置き換え、あくまで単独動作を検証する。

テスト観点(カバレッジ)

• 内部ロジック:演算結果、条件分岐、ループ処理、例外ハンドリング
• インタフェース:入出力パラメータの型・範囲・境界値、呼び出し順序
• 状態管理:状態遷移表/フラグ変数/キャッシュの初期化と破棄
• 異常系:null/nullptr、ゼロ除算、タイムアウト、外部リソース不在など
• コード・データカバレッジ:命令網羅率、分岐網羅率、MC/DC など(安全規格で要求されることが多い)

まずは環境構築 ― JUnit 5 を入れる

Gradle で入れたパターン(推奨)

① プロジェクトひな型

hello-test/
 └─ build.gradle

② build.gradle(最小構成)

plugins {
    id 'java'                       // Java プラグイン
}

java {
    sourceCompatibility = '17'      // JDK17 でビルドする例
    targetCompatibility = '17'
}

repositories {
    mavenCentral()                  // 依存ライブラリ入手先
}

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
    // ↑ Jupiter だけで十分。Platform は transitively 解決される。
}

test {
    useJUnitPlatform()              // ← これを忘れると JUnit5 が起動しない
    // ──オプション例──────────────────────────
    // testLogging { events "passed","failed","skipped" }
    // maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2)
}

③ テスト作成パス

src/main/java/...          アプリ本体
src/test/java/...          テスト(JUnit が自動走査)

④ 実行

$ ./gradlew test    # Linux/macOS
> Task :test
BUILD SUCCESSFUL

IDE各アイコンの意味

IDE では各テストに「▶」マークが出現する。
▶ マーク(再生ボタン)
• テストクラスやテストメソッドの左端(行番号のさらに左)に現れる小さな三角アイコン。
• クリックすると、そのクラス/メソッドだけをテストランナーで実行できる

• アイコンは IDE により微妙に異なるが、IntelliJ IDEA・Eclipse・VS Code いずれも再生ボタン型の三角形で共通。

「テストケース設計」〜正常系・異常系・境界値〜

基本概念の整理

  1. 正常系テスト(Happy Path Testing)
    目的: 仕様通りの正常な処理フローの確認
    対象: 有効な入力値による期待された動作
    重要性: システムの基本機能が正しく動作することを保証

  2. 異常系テスト(Negative Testing)
    目的: エラーハンドリングと例外処理の確認
    対象: 無効な入力値や想定外の状況
    重要性: システムの堅牢性と安全性を保証

  3. 境界値テスト(Boundary Value Testing)
    目的: 境界条件での動作の確認
    対象: 最大値、最小値、境界の内外の値
    重要性: バグが発生しやすい境界での品質を保証

テストケース設計の手順

Step 1: MUTの仕様分析

✓ 入力パラメータの特定
✓ 戻り値の定義
✓ 副作用(状態変更)の確認
✓ 前提条件・事後条件の整理

Step 2: テストケース分類

// 例:ユーザー年齢検証メソッド
function validateAge(age) {
    if (age < 0) throw new Error("負の値は無効");
    if (age > 150) throw new Error("範囲外");
    if (age < 18) return "未成年";
    return "成人";
}
// テストケース設計
const testCases = [
    // 正常系
    { input: 0, expected: "未成年" },      // 境界値
    { input: 17, expected: "未成年" },     // 境界値
    { input: 18, expected: "成人" },       // 境界値
    { input: 30, expected: "成人" },       // 代表値
    { input: 150, expected: "成人" },      // 境界値
    
    // 異常系
    { input: -1, expectError: true },      // 境界値
    { input: 151, expectError: true },     // 境界値
    { input: null, expectError: true },    // 特殊値
    { input: "abc", expectError: true }    // 型違い
];

以下のようにcsv形式でテストケースを作成しファイルパス指定してテスト行うこともある

テストID,分類,入力値,期待結果,エラー期待,境界値区分,説明
TC001,正常系,0,未成年,false,下限境界,有効最小値(0歳)
TC002,正常系,17,未成年,false,分岐境界,未成年の最大値
TC003,正常系,18,成人,false,分岐境界,成人の最小値
TC004,正常系,30,成人,false,代表値,成人の代表値
TC005,正常系,150,成人,false,上限境界,有効最大値(150歳)
TC006,異常系,-1,,true,下限境界外,負の値(境界外)
TC007,異常系,151,,true,上限境界外,範囲外の値(境界外)
TC008,異常系,null,,true,特殊値,null値の処理
TC009,異常系,abc,,true,型違い,文字列型の入力

効果的なテスト設計のための原則

✓ 仕様から始める(推測でテストしない)
✓ 境界を意識する(バグは境界で起きやすい)
✓ 異常系を軽視しない(堅牢性の確保)
✓ テストケースに説明を付ける(保守性向上)
✓ 段階的に詳細化する(複雑さの管理)

この体系的なアプローチにより、品質が高く保守しやすいテストケースを効率的に設計できる。

簡易プログラムとテストコード

以下では、
①とてもシンプルな Java プログラムを用意し、
②JUnit 5 で単体テスト(Unit Test)を書く手順とコードを示し、
③各行が何をしているのか
を丁寧に解説する。
IDE は IntelliJ IDEA/Eclipse/VS Code のどれでも構わないが、ここでは Gradle ビルドを例にする。

1.テスト対象の Java プログラム

ファイル: src/main/java/example/Calculator.java

package example;

/**
* 四則演算のごく簡単なサンプルクラス。
*/
public class Calculator {

   // 足し算
   public int add(int a, int b) {
       return a + b;
   }

   // 割り算(ゼロ除算時は ArithmeticException を投げる)
   public int divide(int a, int b) {
       return a / b;
   }
}

ポイント
• “ビジネスロジック” に当たるメソッド(add / divide)だけを持つ。
• divide は 0 で割ると Java 標準で ArithmeticException が発生するため、例外テストの良い題材になる。

2. 単体テストコード

ファイル: src/test/java/example/CalculatorTest.java

package example;

import org.junit.jupiter.api.Test;

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

/**
 * Calculator の単体テスト。
 */
class CalculatorTest {

    // --- 正常系テスト --------------------------------------------------

    @Test
    void add_足し算が正しい() {
        // Arrange (準備)
        Calculator calc = new Calculator();

        // Act (実行)
        int actual = calc.add(2, 3);

        // Assert (検証)
        assertEquals(5, actual, "2 + 3 は 5 になるはず");
    }

    @Test
    void divide_割り算が正しい() {
        Calculator calc = new Calculator();
        assertEquals(4, calc.divide(8, 2));
    }

    // --- 異常系テスト --------------------------------------------------

    @Test
    void divide_ゼロ除算は例外になる() {
        Calculator calc = new Calculator();

        // 第1引数: 期待する例外クラス
        // 第2引数: 例外を発生させるラムダ式
        assertThrows(ArithmeticException.class,
                     () -> calc.divide(10, 0),
                     "0 で割ったら ArithmeticException が起きるはず");
    }
}

3. コードの読み方・ポイント解説

① @Test アノテーション
• JUnit に「これはテストメソッド」と伝える印。
• 戻り値は void、引数は基本的に無し。

② import static org.junit.jupiter.api.Assertions.*;
• アサーション(assertEquals, assertThrows など)を静的インポート。
• メソッド名だけで呼べるので読みやすい。

③ Arrange–Act–Assert(AAA パターン)
• Arrange: テスト対象のインスタンスや入力を準備。
• Act: メソッドを呼び出し“結果”を得る。
• Assert: 結果が期待どおりか検証。
AAA を段落で分けるとテスト意図がひと目で分かります。

④ assertEquals(expected, actual, message)
• expected → actual の順番を守ると、失敗時のメッセージが「Expected 5 but was 4」のように自然な文になる。
• 第3引数の message は失敗したときだけ表示され、テスト名以上に理由を説明できる。

⑤ assertThrows
• 第1引数に「期待する例外クラス」、第2引数に「例外を発生させるコード」をラムダ式で渡す。
• 例外が出なければテスト失敗、別の例外ならそれも失敗。
• 戻り値として実際に投げられた例外オブジェクトを受け取り、message の検証などもできる(今回は割愛)。

4. 実行結果の見方

• すべてのテストが通れば 緑色/テスト成功

• 失敗すると 赤色 が表示され、どの assert が落ちたかスタックトレースで示される。

依存を切り離す ― Mockitoで “モック” を使いこなす

「単体テストを書こう!」と思った瞬間にぶつかる壁——
それは “外部依存” だ。
DB、Web API、ファイルシステム、ランダム値…
これらに触れるコードをそのままテストすると

・実行が遅い/不安定になる
・ネットワークや認証情報を用意しないと動かない
・副作用(データ汚染)を起こす

──そんなときに登場するのが モック(Mock)。Java では Mockito が定番ライブラリである。

「ユニットテストを書いているつもりなのに、DB が落ちると全部失敗する」
「タイムアウトやレートリミットで CI が不安定」
──それはテスト対象(SUT: System Under Test)が外部依存に強く結び付いているサイン。
Mockito を使って依存を切り離し、高速・決定論的・再現性の高いテストを書く方法を説明する。

1 モック(Mock)とは何か

実際の開発現場では、テストしたいクラスが他のクラスやサービスに依存していることがよくある。例えば、データベースアクセス、外部API呼び出し、ファイル操作など。これらの依存関係があると、単体テストが困難になってしまう。

Mockitoは、こうした依存関係を「偽物(モック)」に置き換えて、テスト対象のコードだけを純粋にテストできるようにするJavaのフレームワークだ。

【テスト対象】が依存しているクラスや外部システムを仮のオブジェクトで置き換える技術。
目的は 2 つ。

①外部の不確実性を排除して、テストを速く・安定させる
②依存オブジェクトへ呼び出しが正しいか検証する

「実際の API を叩かない代わりに “こう振る舞う” オブジェクトを自分で用意する」と覚えれば OK 。

2. Mockito のセットアップ

Gradle 例(JUnit 5 と一緒に使う想定):

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
    testImplementation 'org.mockito:mockito-core:5.11.0'
    // アノテーションベースで書くなら
    testImplementation 'org.mockito:mockito-junit-jupiter:5.11.0'
}

JUnit 4 を使う場合は mockito-inline や mockito-all を入れる方法もありますが、2025 年現在は JUnit 5 + mockito-junit-jupiter が主流。

3. 具体例で理解する

シナリオ:ユーザー管理システム
ユーザーの年齢を計算して成人かどうかを判定するサービスを考えてみる。

3-1. 例題コード

// ユーザー情報を取得するリポジトリ(データベースアクセス)
public interface UserRepository {
    User findById(Long id);
}

// ユーザーエンティティ
public class User {
    private Long id;
    private String name;
    private LocalDate birthDate;
    
    // コンストラクタ、ゲッター、セッター...
}

// テスト対象:ユーザーサービス
@Service
public class UserService {
    private final UserRepository userRepository;
    
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    public boolean isAdult(Long userId) {
        User user = userRepository.findById(userId);
        if (user == null) {
            throw new UserNotFoundException("User not found");
        }
        
        LocalDate today = LocalDate.now();
        int age = Period.between(user.getBirthDate(), today).getYears();
        return age >= 18;
    }
}

問題:依存関係があるとテストが困難

もしモックを使わずにテストしようとすると...

@Test
public void testIsAdult_実際のDB接続が必要() {
    // ❌ 実際のデータベース接続が必要
    // ❌ テストデータの準備が複雑
    // ❌ 他のテストに影響を与える可能性
    // ❌ 実行速度が遅い
    UserRepository realRepository = new DatabaseUserRepository();
    UserService userService = new UserService(realRepository);
    
    // このテストは多くの問題を抱えている...
}

解決:Mockitoでモックを作成

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

public class UserServiceTest {
    
    @Mock
    private UserRepository userRepository;
    
    @InjectMocks
    private UserService userService;
    
    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);
    }
    
    @Test
    void testIsAdult_成人の場合() {
        // Given(テストデータの準備)
        Long userId = 1L;
        LocalDate birthDate = LocalDate.now().minusYears(25); // 25歳
        User mockUser = new User(userId, "田中太郎", birthDate);
        
        // モックの振る舞いを定義
        when(userRepository.findById(userId)).thenReturn(mockUser);
        
        // When(実際の処理実行)
        boolean result = userService.isAdult(userId);
        
        // Then(結果の検証)
        assertTrue(result);
        verify(userRepository).findById(userId); // メソッドが呼ばれたことを確認
    }
    
    @Test
    void testIsAdult_未成年の場合() {
        // Given
        Long userId = 2L;
        LocalDate birthDate = LocalDate.now().minusYears(16); // 16歳
        User mockUser = new User(userId, "佐藤花子", birthDate);
        
        when(userRepository.findById(userId)).thenReturn(mockUser);
        
        // When
        boolean result = userService.isAdult(userId);
        
        // Then
        assertFalse(result);
    }
    
    @Test
    void testIsAdult_ユーザーが存在しない場合() {
        // Given
        Long userId = 999L;
        when(userRepository.findById(userId)).thenReturn(null);
        
        // When & Then
        assertThrows(UserNotFoundException.class, () -> {
            userService.isAdult(userId);
        });
    }
}

まとめ

Mockitoを使ったモックテストは:

テストの焦点を明確に - テスト対象のロジックだけに集中
高速な実行 - 外部リソースに依存しない
安全なテスト - 本番データに影響しない
繰り返し可能 - 常に同じ結果が得られる
モックを適切に活用することで、信頼性の高い単体テストを効率的に書けるようになる。

おわりに

本記事では JUnit 5 と Mockito を使った単体テストの基礎から応用まで紹介してきた。
環境構築、基本的なアサーション、テストケース設計の考え方、そしてモックを使った依存関係の切り離し──
これらはすべて、より良いソフトウェアを作るための土台となる技術である。
品質の高いソフトウェアは、品質の高いテストから生まれる。
まずは小さなメソッドから始めて、テストを書く習慣を身につけていきましょう。

Discussion