tdd-mock-object について
JVM のテストで、遅い依存先や外部依存、不確定な要素をテストダブルで分離する方法を説明するスキルです。5 種類のテストダブルを区別し、境界での利用や契約テスト、過剰なモックの回避を扱います。
主な機能
- 01Dummy、Fake、Stub、Spy、Mock の区別
- 02テストダブルを選ぶための指針
- 03外部依存を分離する方法
- 04契約テストと過剰なモックの注意点
活用シーン
- 01データベース、ネットワーク、ファイルに依存するコードのテスト
- 02時刻など不確定な入力の分離
- 03通知役などの協力オブジェクトの呼び出し確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ yonatankarp/ software-design-skills --skill tdd-mock-objectインストール後、Claude Code で/tdd-mock-objectを入力して読み込まれたか確認します。
詳細
1 行でまとめると
テストから呼び出すには遅すぎる、外部依存が大きすぎる、または非決定的すぎる実際の協力オブジェクトを、テストダブルに置き換えます。ただし、できるだけ使わないようにしましょう。過度なモックは、テストを振る舞いではなく実装に結び付けてしまいます。
このスキルを使う場面
- テストがデータベース、ネットワーク、ファイルシステム、メッセージブローカー、その他の「JVM の外部」にあるリソースに触れてしまう場合。
- テストがシステム時刻、乱数、その他の非決定的な情報源に依存してしまう場合。
- テストが遅い、有料、または呼び出し回数に制限のあるサードパーティ API を呼び出してしまう場合。
- 協力オブジェクトが正しく呼び出されたこと(例:通知機能がトリガーされたこと)を検証したい場合。
このスキルを使わない場面
- 依存先が高速で、決定的で、プロセス内で動作する場合は、実物を使ってください。
- テスト対象となる振る舞いを持つ、自分が所有するクラスをモックする場合。これはコードではなくモックをテストすることになります。
- 値オブジェクトやデータ構造をモックする場合。やりすぎです。実物を構築してください。
- 目につくものをすべてモックした結果、テストの大半がモックの設定になっている場合。モックを増やす前に、設計を簡素化する必要があります。
主な内容
5 つの種類(広く使われている Meszaros の分類):
- ダミー。 実際の値が使われない箇所に渡すプレースホルダーです。(Kotlin の
null、Object()、Any()など。) - フェイク。 簡略化されていますが、動作する実装です。
MutableMap<Id, Entity>を保持するインメモリのRepositoryはフェイクです。 - スタブ。 決められた答えを返しますが、どのように呼び出されたかは検証しません。「
findById(42)を呼び出されたら、このあらかじめ用意した User を返す。」 - スパイ。 呼び出し方を記録するスタブで、テストから呼び出しを検証できます。
- モック。 テストの実行前に期待値を設定し、期待した呼び出しが行われないとテストが失敗します。ライブラリベースのツールには Mockito、MockK、jMock があります。
日常会話では、5 種類すべてを「モック」と呼ぶことが一般的です。厳密に話す場合は、この違いが重要になります。スタブは 入力 を扱い、モックは 出力の検証 を扱います。
ライブラリ(JVM):Mockito(Java の標準)、MockK(Kotlin に適した設計で、final クラスやコルーチンに対応)、jMock(古く、厳格)。Kotlin プロジェクトでは MockK を選び、Java では Mockito を使います。
最大のリスク:過度なモック。 あらゆる協力オブジェクトをモックするテストには、2 つの問題があります。
- 振る舞い ではなく 実装 をテストしてしまいます。どの協力オブジェクトが呼び出されるかを変えるリファクタリングをすると、外部から観察できる振る舞いは同じでも、テストが失敗します。
- テストの大半がモックの設定になります。その結果、テストを読むほうが本番コードを読むより難しくなります。
Beck の「自分が所有していないものをモックしない」ルール。 モックするのは 境界、つまり自分のコードと外部世界とのインターフェースです。外部世界そのものはモックしないでください。境界は 自分たちのもの なので、一貫性のあるフェイクを作れます。外部世界は 相手のもの です。モックは実際の振る舞いから乖離してしまい、本番環境になるまでそのことに気付けません。
モックとコントラクトテストを組み合わせましょう。 単体テストで PaymentGateway をフェイクにする場合は、そのフェイクと実際のゲートウェイの両方に対して実行し、重要なケースで同じ振る舞いをすることを確認するコントラクトテストも作成してください。コントラクトテストがないと、フェイクは実物から乖離し、誤った動作をするようになります。
判断の目安
- まずはモックを使わないことを基本とします。実物 > フェイク > スタブ > スパイ > モックの順です。実用的な範囲で、最も忠実度の高いものを使いましょう。
- 境界 でモックします。ドメイン内では、実物を優先してください。
- 時刻には
Clockを、乱数にはRandomを注入します。HTTP では、サードパーティのクライアントを直接使わず、自分で所有する小さなインターフェースを注入します。 - 「呼び出し回数」の検証は避けましょう。
verify(mock, times(3))は、ほとんどの場合、振る舞いではなく実装をテストしています。 - モックの設定行数がアサーションの行数を上回るテストがあるなら、設計を見直す必要があります。
アンチパターン
- データベースをモックする。 よく見られますが、ほとんどの場合は誤りです。インメモリ実装(H2、実際のデータベースを使う Testcontainers、またはフェイクリポジトリ)を使用してください。
- 値オブジェクト/データクラスをモックする。
data class Money(BigDecimal, Currency)のように実物を構築してください。mock<Money>()は使わないでください。 - フレームワークをモックする。 Spring の
@Transactionalが呼び出されたことをテストで確認しています。テストしているのは自分のコードではなく、Spring です。 - 脆弱な呼び出し回数の検証。 振る舞いに違いがないのに、無害なリファクタリングでリポジトリの呼び出しが 2 回から 1 回に変わり、
verify(repo, times(2))が失敗します。 - フェイクに対するコントラクトテストがない。 フェイクの振る舞いが実物と乖離し、テストは通る一方で、本番環境では問題が発生します。
- 自分が所有していないものをモックする(サードパーティ SDK を直接モックする)。 自分が所有する小さなインターフェースの背後に SDK をラップし、そのインターフェースをモックしてください。
関連項目
tdd-test-isolation— モックは分離を実現するための手法の 1 つです。tdd-anti-patterns— 過剰なモックと脆弱な検証は、カタログにあるアンチパターンです。kp-testing-coroutines—TestDispatcherはディスパッチャーのモックに似た代替手段です。同じ原則が当てはまります。gof-kotlin-idioms— Kotlin の MockK はfinalクラスとサスペンド関数をサポートしています。
参考資料
- Kent Beck、Test-Driven Development By Example、第 27 章 — 「Mock Object」の項目と Self Shunt のバリエーション。
- Gerard Meszaros、xUnit Test Patterns — テストダブルの分類を網羅した資料。
- Steve Freeman & Nat Pryce、Growing Object-Oriented Software, Guided by Tests — 「オブジェクトではなく、役割をモックする」——適切な実践に関する決定版の指針。