Java | オブジェクト指向:モックしやすい設計

Java Java
スポンサーリンク

「モックしやすい設計」とは何か

モックしやすい設計は
「テストのときに、本物の代わりの“ニセモノ(モック)”を簡単に差し込める設計」
のことです。

たとえば、実際にはメールを送らずに「メール送信が呼ばれたかだけ確認したい」とき、
本物のメール送信クラスの代わりに、ログだけ取るダミークラスを差し込める。

これが簡単にできる設計は、総じて

テストを書きやすい
外部サービスやインフラに引きずられない
変更に強い

といったメリットを持っています。

逆に、モックしにくい設計は

クラスの中で勝手に new する
static メソッドを直接呼びまくる
外部サービスにベッタリ結合している

といった特徴を持ち、テストを書くときにすぐ行き詰まります。


まず「モックが必要になる場面」からイメージする

本物を使いたくないもの

テストで「本物を使いたくないもの」は、大体こういうやつです。

メール送信
外部 API 呼び出し(決済サービス、外部認証など)
DB アクセス
現在時刻・乱数・UUID の生成
ファイル I/O

例えばユーザー登録時に「ようこそメール」を送りたいとします。
本番では本当にメールを送りたいけれど、テストで毎回メールが飛んだら困ります。

だからテストでは

実際には送らない
「送信が呼ばれたか」「正しい宛先だったか」だけ検証したい

というニーズが出てきます。

ここで「本物の代わりのニセモノ(モック・スタブ・フェイク)」を差し込める設計になっているかどうかが、モックしやすさの分かれ目です。


モックしにくい設計の典型例

自分の中で依存クラスを new してしまう

悪い例から見てみます。

public class UserService {

    public void register(User user) {
        // ユーザー登録処理…

        SmtpMailSender sender = new SmtpMailSender(); // ここで new
        sender.sendWelcomeMail(user.email());
    }
}
Java

register をテストしたいとき、本当はメールを送りたくありません。
「sendWelcomeMail が呼ばれたか」だけ確認したい。

でも、この設計ではクラスの中で勝手に new SmtpMailSender() しているので、
テスト側から差し替える余地がありません。

モックしにくい設計の特徴は、「依存を外から渡していない」ことです。


モックしやすい設計の基本:依存を「注入」する(重要)

インターフェースに依存するように変える

先ほどのコードを、モックしやすい形に書き直します。

まず、メール送信のインターフェースを定義します。

public interface MailSender {
    void sendWelcomeMail(String email);
}
Java

本番用の実装はこう。

public class SmtpMailSender implements MailSender {
    @Override
    public void sendWelcomeMail(String email) {
        // 実際に SMTP でメール送信する処理
    }
}
Java

UserService は「MailSender に依存する」ようにします。

public class UserService {

    private final MailSender mailSender;

    public UserService(MailSender mailSender) {
        this.mailSender = mailSender;
    }

    public void register(User user) {
        // ユーザー登録処理…

        mailSender.sendWelcomeMail(user.email());
    }
}
Java

もう new SmtpMailSender() はしていません。
必要なもの(MailSender 実装)はコンストラクタで「外から注入してもらう」形です。

これでテスト側は、好きなダミー実装を差し込めるようになります。

テスト用のダミー(モック/スタブ)を作る

テストコード用に、こんなクラスを用意できます。

public class DummyMailSender implements MailSender {

    private boolean called;
    private String lastEmail;

    @Override
    public void sendWelcomeMail(String email) {
        this.called = true;
        this.lastEmail = email;
    }

    public boolean wasCalled() {
        return called;
    }

    public String lastEmail() {
        return lastEmail;
    }
}
Java

テストではこう使えます。

DummyMailSender dummy = new DummyMailSender();
UserService service = new UserService(dummy);

User user = new User("taro@example.com");
service.register(user);

assertTrue(dummy.wasCalled());
assertEquals("taro@example.com", dummy.lastEmail());
Java

本物のメールは一切送らずに、
「メール送信が呼ばれたか」「宛先は正しいか」だけ確認できます。

これが「モックしやすい設計」の基本パターンです。


コンストラクタ注入が基本形になる理由

new してしまうと差し替えづらい

依存をコンストラクタ引数で受け取る形(コンストラクタ注入)にすると、

どの依存を使うかは、UserService の外が決める
UserService はインターフェース(MailSender)だけ知っていればよい
テストではテスト用実装を渡せる

という状態になります。

逆に、クラスの中で直接 new してしまうと、

UserService の中で具象クラスを固定してしまう
テスト側から差し替えられない
本番用の実装とテスト用の実装を切り替える余地がない

という“ガチガチの結合”が生まれます。

「依存する相手は new しない。外からもらう(注入してもらう)」
これを習慣にすると、自然とモックしやすい設計に近づきます。


static 呼び出しとモックの相性の悪さ

直接 static メソッドを呼びまくる設計

例えば「現在時刻」を使うコード。

public class ReportService {

    public Report create() {
        LocalDateTime now = LocalDateTime.now();  // 直接呼んでいる
        // now をもとにレポート作成…
    }
}
Java

これをテストしようとすると、「毎回 now が変わる」ので期待値を固定しにくくなります。
また、LocalDateTime.now() 自体はモックできません。

こういう場合も、「供給者」を挟みます。

public interface Clock {
    LocalDateTime now();
}
Java
public class SystemClock implements Clock {
    @Override
    public LocalDateTime now() {
        return LocalDateTime.now();
    }
}
Java
public class ReportService {

    private final Clock clock;

    public ReportService(Clock clock) {
        this.clock = clock;
    }

    public Report create() {
        LocalDateTime now = clock.now();
        // now を使ってレポート作成…
    }
}
Java

テストでは Clock のモック/スタブを渡せます。

public class FixedClock implements Clock {
    private final LocalDateTime fixed;

    public FixedClock(LocalDateTime fixed) {
        this.fixed = fixed;
    }

    @Override
    public LocalDateTime now() {
        return fixed;
    }
}
Java

こうすれば

new ReportService(new FixedClock(固定の日時))

のようにして、
「常に同じ時間が返ってくる前提でテストする」ことができます。

static 直呼びすると、こういう差し替えが一切できません。
「モックしたいものを、インターフェース+注入に寄せる」のがコツです。


コラボする相手の数を減らすほど、モックが楽になる(重要)

1 メソッドがたくさんの依存を触ると、モックも増える

例えばこんなクラス。

public class BillingService {

    private final OrderRepository orderRepository;
    private final UserRepository userRepository;
    private final DiscountService discountService;
    private final PaymentGateway paymentGateway;
    private final MailSender mailSender;

    public void bill(Long orderId) {
        // orderRepository で注文取得
        // userRepository でユーザ取得
        // discountService で割引計算
        // paymentGateway で決済実行
        // mailSender でメール送信
    }
}
Java

この bill メソッドをユニットテストしようとすると、
上に並んでいる依存を全部モックしなければなりません。

OrderRepository
UserRepository
DiscountService
PaymentGateway
MailSender

モックの数が多いということは、それだけ「このメソッドが一度に多くの責務を持っている」証拠でもあります。

責務を分けることで、モックすべき相手を減らせます。

割引計算を別のクラスに切り出す
決済実行を別の UseCase に分ける

といった形で、「1 メソッドが直接話す相手」の数を減らしていくと、
テストで用意しなければならないモックも減り、テストコードがすっきりします。

モックしやすさは、設計のシンプルさのバロメータでもあります。


モックしなくていいようにする、という発想も大事

純粋なロジックは、そもそもモック不要にする

「何でもかんでもモックすればいい」というわけではありません。
むしろ理想は、

純粋な計算・判定ロジック → モック不要(普通に new してテスト)
外部 I/O や副作用を起こす部分 → モックの切り替えポイント

という切り分けです。

例えば「割引金額の計算」自体は、
外部サービスも DB も要らない、ただのロジックであることが多いです。

public final class DiscountCalculator {

    public int discount(int price, int rate) {
        if (price < 0 || rate < 0 || rate > 100) {
            throw new IllegalArgumentException();
        }
        return price * rate / 100;
    }
}
Java

これはモックしようとする相手ではなく、
テストの「主役」として、そのまま動かせばいいクラスです。

モックしたい相手(外部との境界)と、
モックせずに直接テストしたいロジックを、
頭の中で分けて設計していくと、全体がきれいになります。


まとめ:モックしやすいかどうかを自分に問うチェックポイント

クラスやメソッドを書いたあとに、次のように自問してみてください。

このクラスは、内部で依存クラスを直接 new していないか
外部サービスや現在時刻、乱数などを static で直接呼んでいないか
依存はコンストラクタや setter で「外から注入」できるようになっているか
依存先に対して、インターフェースを挟んでいるか
このメソッドをテストするとき、何個モックを用意しないといけないか

ここで「差し替えにくい」「モックだらけになりそう」と感じたら、
依存をインターフェース+注入に寄せるチャンスです。

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