【Java初心者向け】JUnit & Mockito入門 ― 単体テストの基礎からモックの使い方まで徹底解説

Java

Java でアプリを開発する現場では、「バグが本番環境に出てしまった!」「他の人が書いたコードを修正したら動かなくなった!」という問題がよく起きます。こうした問題を未然に防ぐのが単体テストです。

この記事では、Java のテスト界でデファクトスタンダードとなっている JUnit 5(テストフレームワーク)と Mockito(モックフレームワーク)を使って、初めて単体テストを書く方でも理解できるよう、基礎からステップアップ形式で解説します。

この記事を読むと、単体テストの概念・JUnit の基本アノテーション・Mockito によるモックの作成と検証・実際のコードへの応用が身につきます。


単体テスト(ユニットテスト)とは?

単体テスト(Unit Test)とは、プログラムの最小単位であるメソッドやクラスが、期待通りに動作するかどうかを個別に確認するテストです。

例えば、電卓アプリの「足し算メソッド」だけを単独で動かして「2 + 3 = 5 になるか?」を自動で確認する、というイメージです。人間が手動で全パターンをテストするのは大変ですが、自動テストがあれば毎回確実にチェックできます。

例え話:品質検査の工場ラインに例えると
自動車工場では、エンジン・ブレーキ・タイヤなど各部品を組み立て前に個別に検査します。単体テストもそれと同じで、各クラス・メソッドを個別に検査することで、後からまとめてテストするより早くバグを発見できます。


JUnit とは

JUnit は、Java プログラムの単体テストを行うためのフレームワーク(テストを実行する仕組みの枠組み)です。現在の主流バージョンは JUnit 5(Jupiter とも呼ばれます)です。

JUnit を使うと、テストメソッドに @Test などのアノテーションを付けるだけで、IDE(Eclipse や IntelliJ)やビルドツール(Maven・Gradle)がテストを自動実行してくれます。

アノテーションとは、@ から始まるメタデータの付加情報です。クラスやメソッドに「この処理はテスト用だよ」という意味を付けられます。

JUnit 5 でよく使う主なアノテーションは以下の通りです。

アノテーション説明
@Testテストメソッドであることを示す
@BeforeEach各テストメソッドの実行前に毎回呼ばれる前処理
@AfterEach各テストメソッドの実行後に毎回呼ばれる後処理
@BeforeAllテストクラス全体で一度だけ実行される前処理
@DisplayNameテスト結果に表示される説明文を設定する
@Nestedテストをグループ(ネスト)化して整理する


Mockito とは

Mockito は、Java のユニットテストで使うモックフレームワークです。「モック(Mock)」とは、実際のオブジェクトの代わりに使うニセモノのオブジェクトのことです。

例えば、データベースや外部 API に接続するクラスがまだ完成していなかったり、テスト中に本物のデータベースに接続したくない場合に、Mockito を使えばそのクラスの「ニセモノ」を用意して、あらかじめ決めた値を返すようにできます。

例え:映画の撮影スタンドインに例えると
映画撮影では、主演俳優がいない場面でも撮影を進めるために「スタンドイン(代役)」を使います。Mockito のモックも同じで、依存クラスの代役として振る舞い、テスト対象のクラスだけを単独でテストできるようにします。

Mockito でよく使う主なアノテーションは以下の通りです。

アノテーション説明
@Mockモックオブジェクトを生成するフィールドに付ける
@InjectMocksテスト対象クラスにモックを自動インジェクション(注入)する
@Spy実際のオブジェクトを使いつつ一部だけモック化する
@ExtendWithJUnit 5 で Mockito を有効化するために付ける


環境構築(pom.xml の設定)

Maven を使っている場合、pom.xml に以下の依存関係を追加します。

<!-- JUnit 5 -->
<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter</artifactId>
  <version>5.10.1</version>
  <scope>test</scope>
</dependency>

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

<!-- Mockito JUnit Jupiter 連携 -->
<dependency>
  <groupId>org.mockito</groupId>
  <artifactId>mockito-junit-jupiter</artifactId>
  <version>5.8.0</version>
  <scope>test</scope>
</dependency>

<scope>test</scope> を指定することで、テスト実行時のみこのライブラリが使われます。本番ビルドには含まれないため、アプリの容量を無駄に増やしません。


JUnit の基本:アノテーションと使い方

まず、テスト対象の簡単な Calculator クラスを用意します。

// Calculator.java(テスト対象クラス)
public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }

    public int divide(int a, int b) {
        if (b == 0) {
            throw new IllegalArgumentException("0 で割ることはできません");
        }
        return a / b;
    }
}

次に、JUnit でテストクラスを書きます。テストは Arrange(準備)→ Act(実行)→ Assert(検証) の3ステップで書くことを習慣にしましょう。

// CalculatorTest.java(テストクラス)
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

@DisplayName("電卓クラスのテスト")
class CalculatorTest {

    private Calculator calculator;

    @BeforeEach  // 各テストの前に実行される
    void setUp() {
        calculator = new Calculator();
    }

    @Test
    @DisplayName("足し算:2 + 3 = 5 になること")
    void testAdd() {
        // Arrange(準備)
        int a = 2, b = 3;

        // Act(実行)
        int result = calculator.add(a, b);

        // Assert(検証)
        assertEquals(5, result, "2 + 3 は 5 であるべき");
    }

    @Test
    @DisplayName("ゼロ除算で例外が発生すること")
    void testDivideByZero() {
        assertThrows(
            IllegalArgumentException.class,
            () -> calculator.divide(10, 0)
        );
    }
}

よく使う Assert メソッドをまとめました。

メソッド説明
assertEquals(期待値, 実際値)値が等しいか検証する
assertNotEquals(期待値, 実際値)値が等しくないか検証する
assertTrue(条件)条件が true か検証する
assertFalse(条件)条件が false か検証する
assertNull(値)値が null か検証する
assertThrows(例外クラス, 処理)指定の例外がスローされるか検証する


Mockito の基本:モックの作り方と使い方

OrderService というクラスが、内部で OrderRepository(データベースアクセス)を使っているとします。テスト時に毎回データベースに接続すると、以下のような問題が起きます。

  • データベースが起動していないとテストが失敗する
  • テストの実行が遅くなる
  • テストのたびにデータが変わってしまい結果が安定しない

Mockito を使ってデータベース層をモック化すれば、これらの問題をすべて解決できます。基本的なモックの使い方は以下の通りです。

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

@ExtendWith(MockitoExtension.class)  // Mockito を JUnit 5 で有効化
class OrderServiceTest {

    @Mock                          // モック化する依存クラス
    private OrderRepository orderRepository;

    @InjectMocks                   // テスト対象(モックが自動注入される)
    private OrderService orderService;

    @Test
    @DisplayName("注文IDから注文を取得できること")
    void testGetOrder() {
        // Arrange:モックの振る舞いを定義
        Order mockOrder = new Order(1L, "コーヒー", 500);
        when(orderRepository.findById(1L)).thenReturn(mockOrder);

        // Act:テスト対象メソッドを実行
        Order result = orderService.getOrder(1L);

        // Assert:結果を検証
        assertEquals("コーヒー", result.getName());
        assertEquals(500, result.getPrice());

        // Verify:モックのメソッドが呼ばれたか確認
        verify(orderRepository, times(1)).findById(1L);
    }
}

よく使う Mockito の構文をまとめました。

構文説明
when(...).thenReturn(値)メソッドが呼ばれたときに返す値を設定する
when(...).thenThrow(例外)メソッドが呼ばれたときに例外をスローさせる
verify(モック).メソッド()メソッドが1回呼ばれたことを検証する
verify(モック, times(N)).メソッド()メソッドが N 回呼ばれたことを検証する
verify(モック, never()).メソッド()メソッドが一度も呼ばれていないことを検証する
any() / anyInt()任意の引数をマッチさせるアーギュメントマッチャー


実践例:ショッピングサービスのテスト

複数の依存クラスを持つ ShopService を題材に、実践的なテストの書き方を確認しましょう。まずテスト対象クラスです。

public class ShopService {
    private final ItemRepository  itemRepository;
    private final StockRepository stockRepository;

    public ShopService(ItemRepository itemRepository,
                       StockRepository stockRepository) {
        this.itemRepository  = itemRepository;
        this.stockRepository = stockRepository;
    }

    public boolean purchase(long itemId, int quantity) {
        Item item = itemRepository.findById(itemId);
        if (item == null) {
            throw new IllegalArgumentException("商品が見つかりません");
        }
        int stock = stockRepository.getStock(itemId);
        if (stock < quantity) {
            return false;  // 在庫不足
        }
        stockRepository.decreaseStock(itemId, quantity);
        return true;
    }
}

次に、3つのシナリオを網羅したテストクラスです。

@ExtendWith(MockitoExtension.class)
class ShopServiceTest {

    @Mock private ItemRepository  itemRepository;
    @Mock private StockRepository stockRepository;
    @InjectMocks private ShopService shopService;

    @Test
    @DisplayName("在庫が十分にある場合、購入が成功すること")
    void testPurchaseSuccess() {
        // Arrange
        Item mockItem = new Item(1L, "りんご");
        when(itemRepository.findById(1L)).thenReturn(mockItem);
        when(stockRepository.getStock(1L)).thenReturn(10);  // 在庫10個

        // Act
        boolean result = shopService.purchase(1L, 3);  // 3個購入

        // Assert
        assertTrue(result);
        verify(stockRepository).decreaseStock(1L, 3);  // 在庫が減らされたか確認
    }

    @Test
    @DisplayName("在庫不足の場合、購入が失敗すること")
    void testPurchaseFailDueToStockShortage() {
        // Arrange
        Item mockItem = new Item(1L, "りんご");
        when(itemRepository.findById(1L)).thenReturn(mockItem);
        when(stockRepository.getStock(1L)).thenReturn(2);   // 在庫2個のみ

        // Act
        boolean result = shopService.purchase(1L, 5);  // 5個購入しようとする

        // Assert
        assertFalse(result);
        verify(stockRepository, never()).decreaseStock(anyLong(), anyInt());  // 在庫減少は呼ばれない
    }

    @Test
    @DisplayName("存在しない商品IDを指定すると例外が発生すること")
    void testPurchaseItemNotFound() {
        when(itemRepository.findById(99L)).thenReturn(null);

        assertThrows(
            IllegalArgumentException.class,
            () -> shopService.purchase(99L, 1)
        );
    }
}

テストは Arrange(準備)→ Act(実行)→ Assert(検証) の3ステップ(AAA パターン)で書くことを習慣にしましょう。コードが読みやすくなり、テストの意図が伝わりやすくなります。


まとめ

  • JUnit 5 は Java のテスト実行フレームワーク。@Test@BeforeEach などのアノテーションでテストを構造化できる。
  • Mockito は依存クラスのモック(ニセモノ)を作るフレームワーク。データベースや外部 API への依存を排除してテストを高速・安定に保てる。
  • @InjectMocks でテスト対象クラスを定義し、@Mock で依存クラスをモック化するのが基本パターン。
  • when(...).thenReturn(...) でモックの振る舞いを定義し、verify(...) でメソッドの呼び出しを検証できる。
  • テストは Arrange → Act → Assert(AAA) の3ステップで書くと読みやすくなる。
  • 単体テストを書く習慣をつけることで、バグの早期発見・安全なリファクタリング・ドキュメントの代替という3つのメリットが得られる。


参考リソース

コメント

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