公開メソッドの中で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のリンクにはアフィリエイトを含みます。
いまprivateをモックしたくなった処理を、「どんな条件で、外へ何が起きればよいか」に言い換えてみてください。置き換えるべき相手は、privateメソッドではなく、その奥の依存かもしれません。
