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

テストの「期待する出力」は、どこから来たのか

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

このコースの初回です。テスト・履歴・レビュー・CI を、どれも「何かと何かの比較」として読みます。今回は最も身近な比較、テストの期待する出力の出どころを見ます。

まず結論

テストは、実際の出力を「期待する出力」と比べる作業です。テストランナーが判定するのは一致したかどうかだけで、どちらが正しいかは判定しません。

期待する出力を実装と同じ計算で作ったり、実装の出力から写したりすると、実装の誤りまで写り、テストは緑のままになります。緑が何を意味するかは、期待する出力の出どころで決まります。

? 読む前に予測

テストが期待する出力を `baseUrl + "/albums"` で作っています(`baseUrl` は `"http://photos.google.com/"`)。実装も同じ連結で URL を作っていたら、このテストはどうなるでしょうか。

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

抽象原理

テストは比較で、比べる相手は誰かが用意する

テストは、ある入力に対する実際の出力を、あらかじめ用意した値と比べます。この値を本コースでは期待する出力、略して期待値と呼びます。確率の期待値(平均)とは別物です。

テストランナーがするのは一致の判定だけです。一致すれば緑、しなければ赤。どちらの値が正しいかは判定しません。 緑が意味を持つかは、期待する出力がどこから来たかで決まります。

テストが比べているもの
同じ入力
片方は実装が出し、片方は誰かが用意する
実際の出力実装が出す
期待する出力出どころは3通り
テストランナーは一致するかだけを見る
緑 / 赤どちらが正しいかは判定しない
  • ・期待する出力の出どころ: (1) 仕様から人が書き下す (2) 実装と同じ計算で作る (3) 実装の出力を写す
  • ・(2)(3) は実装から独立していない。実装の誤りが期待する出力にも写る

SWE at Google 12章は、この点を見出し Don’t Put Logic in Tests で扱い、良いテストの条件をこう書きます。

Clear tests are trivially correct upon inspection

一目で正しいと分かること。期待する出力が正しいかを確かめているのは、読んだ人の目です。

具体実物

連結1回で、誤りが期待する出力に入る

同じ章の Example 12-15 は、題が Logic concealing a bug(バグを隠すロジック)です。原文から2行を抜粋します。

String baseUrl = "http://photos.google.com/";
...
assertThat(nav.getCurrentUrl()).isEqualTo(baseUrl + "/albums");

baseUrl は / で終わっているので、期待する出力は http://photos.google.com//albums です。

続く Example 12-16 は、同じ期待する出力を "http://photos.google.com//albums" と誤った値のまま書き下した版です(末尾に // Oops!)。原文はその後でこう書きます。

When the whole string is written out, we can see right away that we’re expecting two slashes in the URL instead of just one. If the production code made a similar mistake, this test would fail to detect a bug.

書き下しても値が誤っていれば見逃します。変わるのは、誤りが読む人の目に見える点です。

原文に実装コードはありません。そこで本教材で、同じ連結をする実装を用意しました。2本目は、12-16 の期待する出力を読んだ人が仕様どおり /albums に直した版です。

本教材の再現用コード(nav.mjs と logic.test.mjs の比較部分)設定ファイル
// 実装: baseUrl が / で終わると // になる
goToAlbumPage() { this.current = this.baseUrl + "/albums"; }
 
// テスト1: 期待する出力を連結で作る(12-15 相当)
assert.equal(nav.getCurrentUrl(), baseUrl + "/albums");
// テスト2: 12-16 の // を / に直して書き下す
assert.equal(nav.getCurrentUrl(), "http://photos.google.com/albums");
node v24.21.0(macOS、2026-09-24)。suites・duration 等の行とスタックトレースを省略コマンド出力
$ node --test logic.test.mjs
✔ 連結で作る(12-15 相当) (0.287208ms)
✖ 仕様から書き下す(12-16 の // を / に直した版) (0.385ms)
ℹ pass 1
ℹ fail 1
+ actual - expected
+ 'http://photos.google.com//albums'
- 'http://photos.google.com/albums'

連結で作ったテストは緑です。実装と同じ計算をしているので、同じ誤りを含んだ値同士が一致しました。//albums を捕まえたのは、人が仕様から正しい値を書き下したテストだけです。12-16 の誤った値のままなら、これも緑でした(本教材で確認)。

✓ ここまでの確認

SWE at Google 12章の Example 12-15 は、なぜ誤りを見逃しうると説明されていますか。

抽象原理

実装から写すのは、いつ誤りで、いつ正当か

Kent Beck は「Canon TDD」(2023)で、テストを通す段階の誤りの1つをこう挙げます。

Mistake: copying actual, computed values & pasting them into the expected values of the test. That defeats double checking, which creates much of the validation value of TDD.

テストの価値は二重確認にあります。実装とは別の経路で出した値と突き合わせるから、ずれが見つかります。期待する出力を実装から写せば、経路が1本になります。

ところが、実際の出力を写すこと自体を手順にしているテストがあります。Michael Feathers の特性テスト(2016)です。

The purpose of characterization testing is to document your system’s actual behavior, not check for the behavior you wish your system had.

実行して出てきた値を貼り付けて緑にする手順を示し、そうしたテストを descriptions of what we have rather than statements of correctness と見られると書きます。正しさの表明ではなく、今の振る舞いの記述です。

2人は逆のことを言っているように見えますが、目的が違います。Beck は新しい振る舞いを作るときの確認、Feathers は既存コードの今の振る舞いの記録です。写した値は実装から独立していない、という前提は同じです。

具体実物

スナップショットは「前回と同じか」を見る

写す作業を道具にしたのがスナップショットテストです。node:test の文書はこう説明します。

Snapshot tests allow arbitrary values to be serialized into string values and compared against a set of known good values.

known good values(正しいと分かっている値)と呼んでいます。本教材で、金額を桁区切りで整形する関数を使って確かめました。

node v24.21.0(macOS、2026-09-24)。各段階の ✔/✖・exit・スナップショットの中身だけを抜き出した要約追跡ログ
(b) --test-update-snapshots で生成 ✔ exit=0
保存された値: "¥0", "¥1,234", "¥1,000,000"
(d) 実装を String(yen) に変える ✖ exit=1
+ '\n[\n "¥0",\n "¥1234",\n "¥1000000"\n]\n'
- '\n[\n "¥0",\n "¥1,234",\n "¥1,000,000"\n]\n'
(e) 壊れたまま --test-update-snapshots ✔ exit=0
保存された値: "¥0", "¥1234", "¥1000000"
(f) フラグなしで再実行 ✔ exit=0

(d) で赤になります。前回と違うことは検出できます。しかし (e) で壊れたまま更新すると、桁区切りの無い値が新しいスナップショットになり、(f) は緑です。

(b) で保存した "¥1,234" が正しいと判断したのは、ツールではなく中身を見た人です。スナップショットの緑が言えるのは「保存したときと同じ」まで。変更で既存の機能が壊れていないかを確かめる回帰の検出は、この「前回との比較」の上に成り立っています。基準にした前回が正しかったかは、別に確かめる必要があります。

✓ ここまでの確認

node:test のスナップショットテストで、実装を壊した状態のまま `--test-update-snapshots` を付けて実行し、次にフラグなしで実行しました。結果はどうなりますか。

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

期待する出力を連結で作ったテスト

期待する出力を baseUrl + "/albums" で作ると、実装と同じ誤りが入る。二重スラッシュを含む値同士が一致して緑になる。

何と何を比べたか表
実際の出力 http://photos.google.com//albums
期待する出力 http://photos.google.com//albums
出どころ 実装と同じ連結
結果 緑
具体例 B

壊れたまま更新したスナップショット

更新フラグを付けると、その時点の実装の出力が保存される。桁区切りが消えた値が基準になり、以後の実行は緑になる。

何と何を比べたか表
実際の出力 "¥1234"
期待する出力 "¥1234"(更新で保存された値)
出どころ 実装の出力をそのまま保存
結果 緑

計算で作った期待する出力と、ツールが保存した期待する出力。仕組みは違いますが、緑が同じように空になる理由は何でしょうか。

それぞれの期待する出力を作るとき、実装を通らない経路が1本でもあったかを探してください。

この章のまとめ

  • テストは実際の出力と期待する出力の比較。テストランナーは一致だけを見て、どちらが正しいかは判定しない。
  • 期待する出力の出どころは3通り。仕様から書き下す / 実装と同じ計算で作る / 実装の出力を写す。後の2つは実装から独立していない。
  • Beck は写すことを二重確認を失う Mistake と呼び、Feathers は既存の振る舞いの記録として写す。目的が違うだけで、写した値が独立していないという前提は同じ。
  • スナップショットの緑は「保存したときと同じ」まで。壊れたまま更新すれば壊れた値が基準になる。
  • 緑を見たら「何と一致したのか」を問う。基準を用意するのは人の仕事。

自分の現場で確かめる

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

環境:

理解度チェック

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

問題 1 / 3正解: 0

Feathers(2016)は、特性テスト(characterization testing)の目的をどう説明していますか。

用語集(4語)
期待する出力(期待値)
テストが実際の出力と比べる相手の値。確率・統計の「期待値」(平均)とは別物。テストランナーは一致を判定するだけで、この値が正しいかは判定しない。
緑 / 赤
テストの成功・失敗の俗称。本コースでは「比べた相手と一致した/しなかった」の意味でだけ使う。
特性テスト (characterization testing)
既存システムの実際の出力を記録して期待する出力にするテスト。望む振る舞いではなく、今の振る舞いを記録する。
スナップショットテスト
出力を文字列にしてファイルに保存し、以後の実行で保存済みの値と比べるテスト。node:test では v22.3.0 で追加され、v23.4.0 で実験的扱いが外れた。