Java初心者向けJUnit&Mockito完全入門|単体テストの基礎からモック・アサーションまで徹底解説

Java

Java開発をしていると、単体テスト(ユニットテスト)は避けて通れません。
しかし、JUnitやMockitoを初めて使うと、「whenっ何をするの?」「verifyっていつ使うの?」
「spyとmockの違いがわからない…」といった疑問が次々と出てきます。

この記事では、JUnit & Mockito初心者の方が迷わずテストコードを書けるように、基礎から応用まで徹底的に解説します。

※ビルドツールはMavenを使用しています。

JUnit 5 ドキュメントはこちら

単体テストとは?

単体テスト(Unit Test)とは、プログラムの最小単位(メソッドやクラス)を対象に、仕様どおりに動作するかを検証するテストです。単体テストを行うメリットとしては、以下が考えられます。

  • 仕様変更時にすぐ不具合を検知できる
  • バグ発生箇所を特定しやすい
  • リファクタリングが安心して行える

JUnitとMockitoの役割

JUnitとMockitoはJavaの単体テストで広く使用されているフレームワークであり、これらは一般的に組み合わせて使用されます。

JUnitは、テストを構造的に記述および実行するためのJavaテストフレームワークです。テストメソッドの定義、テスト環境の構築と終了、そして期待される結果のアサーションを行うためのアノテーション(’@’)が用意されています。

Mockitoは、モックオブジェクトの作成を容易にするJava用のモックフレームワークです。モックオブジェクトは実際の依存関係をモック(疑似的なオブジェクト)したもので、テスト対象クラスを外部依存関係(データベース、外部API、その他サービス)から分離することができます。Mockito を使用し、これらのモックオブジェクトの動作を定義し、それらとの相互作用を検証できるため、テスト対象コードが依存関係の実際の実装に依存することなく、期待どおりに動作することを確認できます。

本質的には、JUnit はテストを構造化して実行するためのフレームワークを提供し、Mockito はモック オブジェクトを作成および管理するためのツールを提供し、ユニット テスト中に依存関係を効果的に分離および制御できるようにします。

モックが必要な理由

例えば以下のようなOrderServiceがあるとします。

class OrderService {
    private PaymentService paymentService;

    public OrderService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }

    public boolean processOrder(String orderId) {
        return paymentService.pay(orderId);
    }
}

もしPaymentServiceが本物のクレジット決済APIをよびだすと

  • 実際に課金されてしまいます
  • テストが遅くなる
  • APIがダウンしているとテストも失敗する

といった問題がおきます。Mockitoでモック化すれば、外部依存を排除して、適当な値を返すだけにモック化し、テスト対象のみに集中できます。

環境構築(Maven)

ではJUnitとMockitoを使用できるようにします。pom.xmlに以下を追加します。
※こちらで指定しているバージョンはプロジェクトごとに最適なバージョンで対応してください。

<dependencies>
    <!-- JUnit5 -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.10.0</version>
        <scope>test</scope>
    </dependency>

    <!-- Mockito -->
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-core</artifactId>
        <version>5.5.0</version>
        <scope>test</scope>
    </dependency>

    <!-- Mockito + JUnit5連携 -->
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-junit-jupiter</artifactId>
        <version>5.5.0</version>
        <scope>test</scope>
    </dependency>
</dependencies>

これでMavenプロジェクトでJUnitとMockitoが使用できるようになります。



JUnitの基本

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

class CalculatorTest {

    @Test
    void add_2plus3_returns5() {
        Calculator calc = new Calculator();
        int result = calc.add(2, 3);
        assertEquals(5, result);
    }
}
  • @Test:テストメソッドを表す
  • assertEquals(expeced, actual) :期待値と実測値の比較

JUnitのアサーション一覧
JUnitでは、期待値と実際の結果を比較するためにアサーションを使います。

メソッド説明
assertEquals(expected, actual) 値が等しいか確認assertEquals(5, calc.add(2, 3));
assertNotEquals(unexpected, actual)値が異なるか確認assertNotEquals(0, result);
assertTrue(condition)条件がtrueか確認assertTrue(result > 0);
assertFalse(condition)条件がfalseか確認assertFalse(list.isEmpty());
assertNull(actual)値がnullか確認assertNull(obj.getData());
assertNotNull(actual)値がnullでないか確認assertNotNull(user);
assertThrows(Exception.class, () -> { … })例外発生を確認assertThrows(IllegalArgumentException.class, () -> service.run(null));



モックとスタブの関係性

Mockitoの使い方を説明する前に、前提知識としてモックとスタブの関係性について紹介しておきます。モックに関しては、冒頭で説明しましたが、こちらで改めて記述しておきます。

モック(Mock)

  • 役割
    本物のオブジェクトの代わりに使うダミーオブジェクト(テスト対象クラスが依存している別のクラスをモック化する)
  • 目的
    テスト対象(SUT:System Under Test)が、その依存オブジェクトに対して「正しいメソッド呼び出しを行ったか」を確認する。→ 呼び出し回数、引数、順序などの検証が可能。
  • 特徴
    ・デフォルトでは何もしない(null/0/falseを返す)。
    ・「振る舞いの設定」(=スタブ化)をしないと、値は返さない。
    ・Mockitoでは @MockMockito.mock() で作成。

スタブ(Stub)

  • 役割
    モックに「こう呼ばれたらこう返す」という“振る舞い(固定値)”を設定すること。
  • 目的
    テスト対象が期待通りの値を受け取ったときに、ロジックが正しく動くか確認する。
  • 特徴
    when(...).thenReturn(...) などで定義(後述)
    ・実際の外部処理を行わず、固定値やダミーの値を返す。

イメージ
本物のATM機 → モック(ATMのふりをする箱)
「カードを入れたら残高を返す」 → スタブ(残高は常に10000円と返すように設定する)

Mockitoの基本

・モック作成

PaymentService paymentService = mock(PaymentService.class);

※Mockitoのメソッドである「when」「doReturn」「vertify」などはモックされたオブジェクトに対して使うもの

・戻り値設定(スタブ)

when(paymentServiceMock).pay("order-123").thenReturn(true);
// when(モックインスタンス).メソッド(任意の引数).thenReturn(任意の戻り値)

 もしくは

doReturn(true).when(paymentServiceMock).pay("order-123");
// doReturn(任意の戻り値).when(モックインスタンス).メソッド(任意の引数);

・戻り値無し(void)

doNothing().when(paymentServiceMock).greet("Hello");
// doNothing().when(モックインスタンス).メソッド(任意の引数);

・メソッド呼び出し回数検証

verify(paymentService, times(1)).pay("order-123");
// verify(モックインスタンス, times(呼び出し回数)).モックメソッド(任意の引数);



実装例

テスト対象クラス(SUT)

public class BankService {
    private final ATM atm;

    public BankService(ATM atm) {
        this.atm = atm;
    }

    public int checkBalance(String accountId) {
        return atm.getBalance(accountId);
    }

    public boolean withdraw(String accountId, int amount) {
        int currentBalance = atm.getBalance(accountId);
        if (currentBalance >= amount) {
            atm.dispenseCash(accountId, amount);
            return true;
        }
        return false;
    }
}

依存クラス(モック化対象)

public interface ATM {
    int getBalance(String accountId);
    void dispenseCash(String accountId, int amount);
}

Mockitoを使ったテスト

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

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

public class BankServiceTest {

    @Test
    void testCheckBalance() {
        // モック作成
        ATM mockATM = Mockito.mock(ATM.class);

        // スタブ設定
        when(mockATM.getBalance("1234")).thenReturn(10000);

        // SUT作成(モックを注入)
        BankService bankService = new BankService(mockATM);

        // 実行
        int balance = bankService.checkBalance("1234");

        // 検証
        assertEquals(10000, balance);
        verify(mockATM, times(1)).getBalance("1234"); // 呼び出し検証
    }

    @Test
    void testWithdrawSuccess() {
        ATM mockATM = mock(ATM.class);

        // 複数回呼び出しに対応するスタブ設定
        when(mockATM.getBalance("1234"))
                .thenReturn(10000) // 最初の呼び出し
                .thenReturn(7000); // 引き出し後の想定値

        BankService bankService = new BankService(mockATM);

        boolean result = bankService.withdraw("1234", 3000);

        assertTrue(result);
        verify(mockATM, times(1)).getBalance("1234");
        verify(mockATM, times(1)).dispenseCash("1234", 3000);
    }

    @Test
    void testWithdrawFailInsufficientFunds() {
        ATM mockATM = mock(ATM.class);

        when(mockATM.getBalance("1234")).thenReturn(2000);

        BankService bankService = new BankService(mockATM);

        boolean result = bankService.withdraw("1234", 5000);

        assertFalse(result);
        verify(mockATM, times(1)).getBalance("1234");
        verify(mockATM, never()).dispenseCash(anyString(), anyInt());
    }
}

上記コードで使われた主要なMockitoメソッド

メソッド/構文用途
mock(Class classToMock)指定クラスのモックを作成
when(mock.method(…)).thenReturn(value)スタブ化(呼び出し時に返す値を設定)
when(…).thenReturn(…).thenReturn(…)複数回の呼び出しごとに異なる戻り値を設定
verify(mock)モックの呼び出しを検証
verify(mock, times(n))指定回数呼び出されたか検証
verify(mock, never())呼び出されなかったことを検証
anyString(), anyInt()引数マッチャー(任意の値を許可)
assertEquals(expected, actual)値が一致するか確認(JUnit)
assertTrue(condition), assertFalse(condition)条件が真/偽か確認(JUnit)


疑問:ATMインターフェースのgetBalanceの実装ロジックはいらないのでしょうか?
今回の例では、ATMインターフェース として定義されているだけなので、実際のロジック(getBalance のオーバーライド実装)は書かれていません。理由は、Mockitoのテストでは 「本物の実装」ではなく、Mockitoが自動生成したモックオブジェクト を使うからです。モックは「依存クラスを実装した匿名の偽オブジェクト」をその場で作り、when(...).thenReturn(...) で振る舞いを設定します。もちらん本番環境にデプロイ時にはgetBalanceメソッドの実装は必要だと思いますが、あくまで現在は単体テストを行いたいため、インターフェースをモックし、モック化したオブジェクトのメソッドが返す値を固定しています。



最後に

この記事では、JUnit & Mockitoを使った単体テストの基礎から、モック・アサーションの使い方まで網羅的に解説しました。これらを理解すれば、外部依存を排除しつつ安全かつ効率的なテストコードが書けるようになります。ぜひプロジェクトに導入して、安心できる開発環境を構築してください。

コメント

タイトルとURLをコピーしました