テストにMockitoを使ってみよう!

docs

 

以前、デザインパターンについて学んだよね。オブジェクト指向の知識が深まって、設計の引き出しが増えたんじゃないかな? 今回は、そんな設計されたコードがちゃんと動くか確認するための大事なプロセス、「テスト」について、特にMockitoという便利なツールに焦点を当てて解説していくよ!


テストってなんで必要なの?

「テスト」って聞くと、「めんどくさい」「時間がない」って思う人もいるかもしれないね。でも、プログラムを開発する上で、テストは品質を保証するために絶対に欠かせないものなんだ。

想像してみてほしい。せっかく頑張って書いたプログラムなのに、いざ動かしてみたら全然意図しない動きをしたり、変なエラーが出たりしたらどうかな? それが本番環境で起きたら、もっと大変なことになるよね。

テストは、そういった問題を開発の早い段階で見つけるための活動なんだ。テストをしっかり書くことで、次のようなメリットがあるよ。

  • バグの早期発見:問題が小さいうちに見つけられるから、修正にかかるコストが少ない。
  • 品質の向上:バグが減り、安定したプログラムを提供できる。
  • 安心して変更できる:既存の機能が壊れていないか、テストで確認しながらコード修正ができる。
  • 設計の改善:テストしやすいコードは、自然と良い設計になることが多い。

特にJavaのプロジェクトでは、JUnitというフレームワークを使ってテストを書くのが一般的だよ。前回の記事でも少し触れたけど、JUnitは単体テスト(メソッドやクラス単位の小さなテスト)を書くための標準的なツールなんだ。


でも、テストって難しい時があるよね?

JUnitを使えばテストは書けるんだけど、実際の業務システムでは、あるクラスが他のクラスに依存している(「依存関係」ってやつだね)ことがほとんどだよね。

例えば、商品の購入処理を行うクラスがあったとして、そのクラスが「在庫管理クラス」や「決済サービス」に依存しているとする。購入処理のテストをしたいのに、そのためだけに本物の在庫管理システムや決済サービスを動かすのは大変だよね? データを用意したり、外部サービスに接続したり、テストのたびに実際に支払いが発生しちゃったり…なんてことになったら、テストどころじゃない。

こんなとき、どうすればいいんだろう?

そこで登場するのが、「モック(Mock)」という考え方と、それを簡単に作れるMockitoというライブラリなんだ!


Mockitoってなに?

Mockito(モキート) は、テストのためにモックオブジェクトを簡単に作成・設定するためのJavaライブラリだよ。

モックオブジェクトというのは、簡単に言うと本物のオブジェクトの「ふり」をする偽物のオブジェクトのこと。本物と同じインターフェース(メソッドの定義)を持っているけど、中身はテストのために都合よく振る舞うように設定できるんだ。

Mockitoを使うことで、以下のようなことができるようになるよ。

  • 依存関係を切り離す:テストしたいクラス(テスト対象)が依存している外部のサービスやクラスをモックに置き換えることで、テスト対象だけを独立してテストできる。
  • 特定の条件を再現する:例えば、メソッドが特定の値を返すように設定したり、例外を発生させるように設定したりできる。
  • メソッドが呼ばれたか確認する:テスト対象のクラスが、依存しているモックオブジェクトの特定のメソッドをちゃんと呼び出したかどうかを確認できる。

これによって、テストの実行が高速になり、安定し、再現性が高まるんだ。


具体例で見てみよう!〜商品購入処理のテスト〜

じゃあ、実際にMockitoを使って、前述の「商品購入処理」の例をテストしてみよう。

まずは、商品購入に関連するクラスを定義するね。

Java

// 在庫管理サービス
interface StockService {
    boolean checkStock(String productId, int quantity); // 在庫確認
    void decreaseStock(String productId, int quantity); // 在庫削減
}

// 決済サービス
interface PaymentService {
    boolean processPayment(double amount); // 支払い処理
}

// 商品購入サービス(テスト対象)
class OrderService {
    private StockService stockService;
    private PaymentService paymentService;

    // コンストラクタインジェクションで依存を注入
    public OrderService(StockService stockService, PaymentService paymentService) {
        this.stockService = stockService;
        this.paymentService = paymentService;
    }

    public boolean purchaseProduct(String productId, int quantity, double price) {
        // 1. 在庫確認
        if (!stockService.checkStock(productId, quantity)) {
            System.out.println("在庫が足りません。");
            return false;
        }

        // 2. 支払い処理
        double totalAmount = price * quantity;
        if (!paymentService.processPayment(totalAmount)) {
            System.out.println("支払い処理に失敗しました。");
            return false;
        }

        // 3. 在庫削減
        stockService.decreaseStock(productId, quantity);
        System.out.println("商品を購入しました!");
        return true;
    }
}

OrderServiceクラスがStockServicePaymentServiceに依存しているのがわかるよね。

次に、このOrderServiceをJUnitとMockitoを使ってテストするコードを見てみよう。


Mockitoを使ったテストコード

Java

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.*; // Mockitoの主要なメソッドをimport

public class OrderServiceTest {

    @Mock // StockServiceのモックを作成
    private StockService mockStockService;

    @Mock // PaymentServiceのモックを作成
    private PaymentService mockPaymentService;

    @InjectMocks // モックを注入してOrderServiceのインスタンスを作成
    private OrderService orderService;

    @BeforeEach // 各テストメソッドの実行前に呼ばれる
    void setUp() {
        // アノテーション(@Mock, @InjectMocks)を有効にする
        MockitoAnnotations.openMocks(this);
    }

    @Test
    void testPurchaseProduct_Success() {
        // 準備(スタブの設定):モックの振る舞いを定義
        // mockStockServiceのcheckStockメソッドが呼ばれたらtrueを返す
        when(mockStockService.checkStock("p001", 1)).thenReturn(true);
        // mockPaymentServiceのprocessPaymentメソッドが呼ばれたらtrueを返す
        when(mockPaymentService.processPayment(1000.0)).thenReturn(true);

        // 実行:テスト対象のメソッドを呼び出す
        boolean result = orderService.purchaseProduct("p001", 1, 1000.0);

        // 検証(アサート):結果が期待通りか確認
        assertTrue(result);

        // 検証(モックの呼び出し検証):モックのメソッドが適切に呼ばれたか確認
        // mockStockServiceのcheckStockが"p001", 1で1回呼ばれたこと
        verify(mockStockService, times(1)).checkStock("p001", 1);
        // mockPaymentServiceのprocessPaymentが1000.0で1回呼ばれたこと
        verify(mockPaymentService, times(1)).processPayment(1000.0);
        // mockStockServiceのdecreaseStockが"p001", 1で1回呼ばれたこと
        verify(mockStockService, times(1)).decreaseStock("p001", 1);
    }

    @Test
    void testPurchaseProduct_OutOfStock() {
        // 準備:在庫がないケースを想定
        when(mockStockService.checkStock("p001", 1)).thenReturn(false);

        // 実行
        boolean result = orderService.purchaseProduct("p001", 1, 1000.0);

        // 検証
        assertFalse(result); // 在庫がないのでfalseが返るはず

        // 在庫確認は呼ばれたが、支払い処理と在庫削減は呼ばれていないことを確認
        verify(mockStockService, times(1)).checkStock("p001", 1);
        verify(mockPaymentService, never()).processPayment(anyDouble()); // どんなdouble値でも呼ばれていない
        verify(mockStockService, never()).decreaseStock(anyString(), anyInt()); // どんな引数でも呼ばれていない
    }

    @Test
    void testPurchaseProduct_PaymentFailed() {
        // 準備:在庫はあるが、支払い失敗するケースを想定
        when(mockStockService.checkStock("p001", 1)).thenReturn(true);
        when(mockPaymentService.processPayment(1000.0)).thenReturn(false);

        // 実行
        boolean result = orderService.purchaseProduct("p001", 1, 1000.0);

        // 検証
        assertFalse(result); // 支払い失敗なのでfalseが返るはず

        // 在庫確認と支払い処理は呼ばれたが、在庫削減は呼ばれていないことを確認
        verify(mockStockService, times(1)).checkStock("p001", 1);
        verify(mockPaymentService, times(1)).processPayment(1000.0);
        verify(mockStockService, never()).decreaseStock(anyString(), anyInt());
    }
}

コードの解説

  1. @Mock アノテーション:
    • @Mock private StockService mockStockService;
    • @Mock private PaymentService mockPaymentService;
    • これでStockServicePaymentServiceモックオブジェクトが自動的に作られるよ。本物の実装じゃなくて、Mockitoが生成した「偽物」のインスタンスだ。
  2. @InjectMocks アノテーション:
    • @InjectMocks private OrderService orderService;
    • これはテスト対象のインスタンス(今回はOrderService)を生成して、そこに@Mockで定義したモックオブジェクトを自動的に注入(インジェクション) してくれる、すごく便利なアノテーションだよ。これで、OrderServiceは本物のStockServicePaymentServiceではなく、モックと連携してテストされることになるんだ。
  3. @BeforeEachMockitoAnnotations.openMocks(this);:
    • 各テストメソッドが実行される前に、@Mock@InjectMocksなどのアノテーションを有効にするために、MockitoAnnotations.openMocks(this);を呼び出す必要があるんだ。これはMockitoを使うときの「おまじない」みたいなものだと思ってね。
  4. when(...).thenReturn(...) (スタブ設定):
    • when(mockStockService.checkStock("p001", 1)).thenReturn(true);
    • これがMockitoの最も基本的な使い方の一つだよ。「mockStockServicecheckStockメソッドが"p001"1という引数で呼ばれたら、trueを返してね」というように、モックのメソッドが特定の引数で呼ばれたときの「振る舞い」を設定しているんだ。これを「スタブ」を設定すると言うよ。
  5. verify(...) (メソッド呼び出し検証):
    • verify(mockStockService, times(1)).checkStock("p001", 1);
    • これは「モックの特定のメソッドが、指定された回数、指定された引数でちゃんと呼び出されたか」を検証するためのものだよ。
      • times(1):1回呼ばれたこと。
      • never():一度も呼ばれていないこと。
      • anyString(), anyInt(), anyDouble():引数の値は何でも良い、という意味だよ。

Mockitoを使うメリット

  • 単体テストが容易になる: 依存関係のあるクラスをモックに置き換えることで、テスト対象のクラスだけを独立してテストできる。
  • テストの実行が高速: 実際の外部サービス(DB、APIなど)にアクセスしないので、テストが速く終わる。
  • テストの安定性が向上: 外部要因(ネットワーク遅延、DBのデータ状態など)に左右されずに、常に同じ結果が得られる。
  • 例外的な状況を簡単にテストできる: 本物のサービスでは再現が難しい「エラー発生時」や「特定のデータが返ってきた時」などのシナリオも、モックを使えば簡単に再現してテストできる。

まとめ

今回はテストの重要性と、特に依存関係の多いテストで役立つMockitoについて学んだね。

  • テストは、プログラムの品質を保証し、バグを早期発見するために不可欠。
  • Mockitoは、テストのためにモックオブジェクトを簡単に作れるライブラリ。
  • モックを使うことで、依存関係を切り離し、テスト対象のクラスだけを独立してテストできる。
  • when(...).thenReturn(...) でモックの振る舞いを定義し、verify(...) でメソッドの呼び出しを確認する。

Mockitoを使いこなせるようになると、より効率的で信頼性の高いテストを書けるようになるよ。最初は少し戸惑うかもしれないけど、実際にコードを書いてみて、その便利さをぜひ体験してみてほしいな!


次回の記事も読んでくれると嬉しいな!

コメント

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