privateをモック? 境界を変えよう。 置き換えたいのは、外部の通信。

広告 設計・テスト・レビュー

privateメソッドをモックしたい?通信の失敗をテストするなら、置き換える場所を変える

公開メソッドの中でprivateメソッドを呼び、その中で外部へ通知している。通知の失敗をテストしたくて「Mockito private モック」と検索した。

この場合、置き換えたいのはprivateという実装上の区切りではなく、外部通信です。この記事では、通知サービスを例に、privateの名前に依存せず、通信失敗時の動きを確認する形へ変えます。

新しく書くテストでは、まず通信を差し替えられる依存にする。
純粋な整形処理は公開メソッドの結果から確認し、成功・失敗を作りたい外部依存を置き換えます。既存のPowerMockテストを保守する場合は、別の作業として実行環境を固定します。

実行サンプル

通知文・呼出回数・例外の伝播・不正入力を、実際の通信を使わずに検証します。 JDK 17以上とMavenを使います。ZIPを展開し、mvn clean testを実行してください。必要なソース、テスト、実行設定を収録しています。

Mockitoでprivateメソッドを直接モックする機能はない

Mockito公式FAQは、privateメソッドのモックを提供しないと説明しています。staticやfinalを扱えるかという話とは別です。

「モックしたいからpublicへ変える」のも、最初の選択にはしません。呼び出し元に公開する必要がない内部処理を、テストの都合だけでAPIに増やすからです。

先に、テストの目的を「通知に失敗したら、呼び出し元へ失敗を伝える」のように言い換えます。その結果を作るには、何を置き換えればよいかを考えます。

通知送信だけを受け取る形にする

次はJava標準APIだけで動く説明用のコードです。通知本文の整形はprivateのまま、外部送信だけを依存として受け取ります。

interface Sender {
    void send(String text);
}

class NotificationService {
    private final Sender sender;

    NotificationService(Sender sender) {
        this.sender = sender;
    }

    public String notify(String subject) {
        if (subject == null || subject.isBlank()) {
            throw new IllegalArgumentException("件名が必要です");
        }
        sender.send(format(subject));
        return "送信済み";
    }

    private String format(String subject) {
        return "通知: " + subject;
    }
}

この例の契約は、空の件名を拒否し、送信が成功したときだけ「送信済み」を返すことです。送信先の実行時例外は呼び出し元へ伝播します。リトライや送信履歴の保存までは扱いません。

成功と失敗を、通信せずに作れる

次のテスト全文で、成功時の戻り値と送信文、失敗時に同じ例外が届くこと、空白の件名で送信しないことを確認します。

import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;

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

class NotificationServiceTest {
    @Test
    void sendsOneFormattedMessageAndReturnsSuccess() {
        List<String> sent = new ArrayList<>();
        var service = new NotificationService(sent::add);

        String result = service.notify("処理完了");

        assertEquals("送信済み", result);
        assertEquals(List.of("通知: 処理完了"), sent);
    }

    @Test
    void propagatesTheSenderFailure() {
        var failure = new IllegalStateException("送信先で失敗");
        List<String> attempted = new ArrayList<>();
        var service = new NotificationService(text -> {
            attempted.add(text);
            throw failure;
        });

        assertSame(failure, assertThrows(IllegalStateException.class, () -> service.notify("処理完了")));
        assertEquals(List.of("通知: 処理完了"), attempted);
    }

    @Test
    void doesNotSendAnEmptySubject() {
        List<String> sent = new ArrayList<>();
        var service = new NotificationService(sent::add);

        assertThrows(IllegalArgumentException.class, () -> service.notify(" "));
        assertEquals(List.of(), sent);
    }
}

privateメソッドの名前は、テストから指定していません。内部の整形方法を変えても、公開された結果と送信内容が同じなら、その契約を引き続き確認できます。

この小さな例はJava 17.0.8で、成功時の結果と送信文、空白の拒否と未送信、送信失敗の伝播を確認しています。Mockito・PowerMockを動かした検証ではありません。

privateを全部別クラスにする必要はない

分ける基準はアクセス修飾子ではなく、依存の性質です。

処理今回の推奨
渡された文字列を整形する公開メソッドの結果や送信内容から確認する
通信、DB、ファイルなどの外部依存必要な成功・失敗を作れる境界として分ける
長大な業務判断がprivateに詰まっている独立した責務として説明できるか見直す

一行の整形のためにクラスを増やすと、読む場所だけ増えることもあります。一方、外部通信まで同じクラスへ閉じ込めているなら、テスト以外にも変更理由を分ける価値があります。

既存のPowerMockテストは、環境ごと扱う

すでにPowerMockでprivateを指定しているテストの保守なら、いきなり全廃はしません。まずCIで動いているJDK、JUnit、Mockito、PowerMockの版と実行コマンドを記録し、対象テストを再現します。

PowerMock公式Wikiには、Runnerや@PrepareForTestなどの準備、互換性に関する注意があります。数行のPowerMockito呼び出しだけを抜き出して、現在の環境へ追加する手順にはしないでください。

今回、PowerMockの組み合わせの再検証はしていません。新規の推奨手順として未検証の依存セットを載せるのではなく、上の通信境界を使う方法を勧めます。変更承認の範囲が狭い保守では、既存テストを維持し、今回触る境界から少しずつ移す判断もできます。

モックの操作より、何を保証するかを学ぶ一冊

『単体テストの考え方/使い方』は、テストを実装の細部へ結びつけすぎず、何を保証するか考えたい人向けです。C#の例を使う本なので、JUnitの書き方を覚える目的とは分けて選びます。出版社の紹介

実務担当の本棚を見る(Amazon)

※Amazonのリンクにはアフィリエイトを含みます。

いまprivateをモックしたくなった処理を、「どんな条件で、外へ何が起きればよいか」に言い換えてみてください。置き換えるべき相手は、privateメソッドではなく、その奥の依存かもしれません。

-設計・テスト・レビュー
-, ,