Dev Study
← 開発の実務
レッスン 3 / 12·約 8 分·第16期 開発の実務

テストダブルは、相手についての仮定を書き込んだもの

リクエストがアプリケーションに届くまで
  1. ブラウザURL入力
  2. DNS名前→IP
  3. ルータ経路選択
  4. ロードバランサ振り分け
  5. Nginx受け口
  6. アプリ処理今ここ
  7. DB保存

前回は、わざと壊した版と比べて、テストが落ちられるかを確かめました。今回は、テストが相手を偽物に差し替えたとき、比べている相手が何になるのかを見ます。

まず結論

「壊れていない」は、何と比べたかでしか言えません。テストダブル(本物の代わりに置く偽物)を使ったテストが比べている相手は本物ではなく、書いた人が仮定した相手です。

隔離すれば速く安定したテストになります。その代わり、仮定が本物とずれていても、テストは緑のまま通ります。

? 読む前に予測

id を数値で返すモック相手には通るテストがあります。同じテストを、id を文字列 "42" で返す本物の API 相手に走らせると、どうなるでしょうか。

当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。

抽象原理

差し替えた瞬間、比べる相手が変わる

テストは、実際の出力を何かと比べて緑か赤かを決めます。API や DB を呼ぶコードなら、その相手が返したものを使って計算した結果と比べます。

ここで相手を偽物に差し替えると、テストが相手にしているのは本物ではなくなります。偽物の応答は誰かが書いたものです。書いた人が「相手はこう返すだろう」と仮定したものです。

xunitpatterns.com に残る Meszaros の草稿は、偽物に求めるものをこう書いています。

The Test Double doesn't have to behave exactly like the real DOC; it merely has to provide the same API as the real one so that the SUT thinks it is the real one!

DOC は依存先、SUT はテスト対象です。振る舞いは本物と完全に同じでなくてよい、同じ API を備えて本物だと思わせればよい。同じページは別の節で、注意もしています。

So we really should have at least one test that verifies it works without a Test Double.

同じテストでも、相手を差し替えると比べているものが変わる
canEdit のテストsession.userId は数値 42
相手を選ぶ
モック{ id: 42 } を返す(書いた人の仮定)
本物の API{ id: "42" } を返す
比べた結果
緑仮定の相手とは合っている
赤false !== true
  • ・モックの緑が言えるのは「仮定が正しければ動く」まで
  • ・本物の応答はどちらの場合も同じで、変わったのは比べた相手だけ
具体実物

モックは数値、本物は文字列

編集ボタンを出すかどうかを決める関数です。セッションの userId は数値で持っています。

export async function canEdit(api, session) {
  const me = await api.getMe();
  return me.id === session.userId;
}

テストは2本。1本目はモックに { id: 42, name: "kazu" } を返させます(コメントに「仮定: id は数値」)。2本目は、手元で立てた本物役のサーバを相手にします。このサーバは DB の大きな id が丸めで変わらないよう、id を文字列 "42" で返します。これは本教材の設定で、id を文字列で返す選択肢は『JSONとAPI — ブラウザ以外の相手に返す』の比較で見たものです。

$ node --test app.test.mjs(Node v24.21.0。集計行の一部とスタックトレースを省略)コマンド出力
✔ モック相手: 自分の投稿なら編集できる (0.300583ms)
✖ 本物の API 相手: 自分の投稿なら編集できる (12.038916ms)
ℹ tests 2
ℹ pass 1
ℹ fail 1
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
false !== true

2回走らせて、2回とも同じ結果でした。canEdit は1文字も変えていません。本物の応答も変えていません。変わったのは比べた相手だけです。

草稿の言う「同じ API」で見ると、呼び出し口の getMe はモックにもあります。ずれているのは返ってくる値の形、id の型です。モックが本物の形まで写していなかった、つまり振る舞いではなく形についての仮定が外れていた、と言えます。

✓ ここまでの確認

モック相手の緑が確かめていたのは、何ですか。

抽象原理

隔離が買うものと、払うもの

では、ダブルは使わないほうがよいのでしょうか。そうではありません。外部依存を偽物に置き換えて対象を隔離すれば、相手の状態や遅延に左右されず、速く安定したテストになります。

SWE at Google 第13章は、ダブルがどれだけ本物に似ているかを忠実度(Fidelity)と呼びます。

Fidelity refers to how closely the behavior of a test double resembles the behavior of the real implementation that it’s replacing.

そのうえで But perfect fidelity might not be feasible と認めます。ダブルは多くの場合、テストに使えるよう本物よりずっと単純にする必要があるからです。だから補い方を書きます。

Unit tests that use test doubles often need to be supplemented by larger-scope tests that exercise the real implementation.

同じ章は Prefer Realism Over Isolation と見出しを立て、本物を優先してよい条件を fast, deterministic, and has simple dependencies と挙げます。条件を満たさない相手なら隔離する理由が残ります。

つまり取引です。隔離は速さと安定を買い、代わりに相手についての仮定を1つ増やします。 増えた仮定は、本物と比べる別のテストでしか確かめられません。

具体実物

テスト用の DB を、別の製品にする

ダブルは API のモックに限りません。Fowler の要約はフェイク(動く実装だが近道をしたもの)の例に an in memory database を挙げます。テストだけ本番と別の DB 製品や別の設定で走らせるのも、同じ差し替えです。

このとき持ち込まれる仮定は、応答の形より見えにくいものです。『既定値が違う2つのDB — 同じSQLが、同じようには走らない』で見たとおり、分離レベルの既定値は InnoDB が REPEATABLE READ、PostgreSQL が READ COMMITTED で、製品によって逆側にずれています。

本番とテストで製品が違えば、同じ SQL を流しても、同時に走るトランザクションから何が見えるかという前提が揃っていません。テストが緑でも、それは「テスト用 DB の既定値で動かしたら通った」までです。

id の型は応答を1つ見れば気づけます。分離レベルは同時に走るトランザクションどうしの見え方の取り決めなので、テストが同時に走らせなければ差は表に出にくいままです。仮定が見えにくいほど、緑が長く続きます。

✓ ここまでの確認

SWE at Google 第13章は `Prefer Realism Over Isolation` という見出しを立てています。本物の実装を優先してよい条件として挙げているのはどれですか。

⇄ 2つを見比べる
具体例 A

API のモックが id を数値で返す

モックの応答は書いた人の仮定でした。本物は id を文字列で返しており、厳密比較が false になります。例外は出ず、画面から編集ボタンが消えるだけです。

仮定と本物表
項目 モック(仮定) 本物
id の型 数値 42 文字列 "42"
テスト 緑 赤
具体例 B

テスト用 DB を別の製品にする

テスト用 DB が持ち込むのは、既定の分離レベルという仮定です。応答を1つ見ても気づけず、トランザクションを同時に走らせないテストでは差が表に出にくいものです。

既定の分離レベル(default-isolation-levels より)表
製品 既定の分離レベル
InnoDB (MySQL) REPEATABLE READ
PostgreSQL READ COMMITTED

API のモックとテスト用 DB。差し替えたものは違いますが、2つのテストの緑は、それぞれ何と比べた結果でしょうか。

テストの相手が返した値や見せた状態は、誰がどこで決めたものかを考えてみてください。

この章のまとめ

  • ダブルを使ったテストが比べている相手は、本物ではなく書いた人が仮定した相手。
  • 草稿は、ダブルは振る舞いまで同じでなくてよく、同じ API を備えればよいと書く。今回の失敗は形(id の型)の仮定が外れたもの。
  • 本物相手では false !== true。例外は出ないので、本番では静かに編集ボタンが消える。
  • 隔離は速さと安定を買う。代わりに仮定が1つ増え、perfect fidelity might not be feasible。
  • 補い方は、本物を動かすより広い範囲のテスト、少なくとも1本のダブル無しのテスト。
  • テスト用 DB の製品違いは、既定の分離レベルという見えにくい仮定を持ち込む。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 3正解: 0

xunitpatterns.com の草稿は、テストダブルが本物と何を同じにすればよいと書いていますか。

用語集(4語)
テストダブル
テストのために本物の代わりに置く偽物の総称。映画のスタントダブルから来た名前(Fowler の説明)。本教材では「相手についての仮定を書き込んだもの」と捉える。
スタブ / モック / フェイク
Fowler の要約では、スタブは決められた応答を返すもの、モックは受けるはずの呼び出しを期待として持つもの、フェイクは動く実装だが本番には向かない近道をしたもの(インメモリ DB が例)。
忠実度 (Fidelity)
SWE at Google 第13章の語。ダブルの振る舞いが、置き換えた本物の振る舞いにどれだけ似ているか。
緑 / 赤
テストが通った状態 / 落ちた状態。本コースでの呼び方。