- ブラウザURL入力
- DNS名前→IP
- ルータ経路選択
- ロードバランサ振り分け
- Nginx受け口
- アプリ処理今ここ
- DB保存
前回は、偽物の相手が書いた人の仮定であることを見ました。今回は同じコードを何度も走らせます。
まず結論
Google Testing Blog(2016)は、同じコードで成功と失敗の両方を示すテスト結果を "flaky" と呼びます。この回で比べるのは、期待する出力ではなく、同じコードを走らせた実行同士です。
コードが同じなのに結果が違うなら、結果を決めた何かがテストの外にあります。時刻・タイムゾーン・待ち時間・実行順。それをテストの中で固定するまで、緑は「その時、その機械では」の結果にすぎません。
9時〜18時を営業中と判定する関数のテストで、`t.mock.timers.enable({ apis: ["Date"], now })` により「今」を 2026-09-24T03:00:00Z に固定しました。これで東京の機械でもロンドンの機械でも結果は同じになるでしょうか。
当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。
同じコードを、もう1回走らせて比べる
このコースの芯は「『壊れていない』は、何と比べたかでしか言えない」です。テストは普通、実行結果を期待する出力と比べます。この回が比べるのは、同じコードを走らせた、もう1回の実行です。
Google Testing Blog は2016年にこう定義しました。
We define a "flaky" test result as a test that exhibits both a passing and a failing result with the same code.
定義されているのは "flaky" なテスト結果です。コードが同じなのに緑と赤が両方出るなら、結果を決めた何かがコードの外にあります。SWE at Google 11章はその候補を挙げます。
Software provides many sources of nondeterminism: clock time, thread scheduling, network latency, and more.
テストの結果は、コードと期待する出力だけで決まるのではありません。時計、スケジューリング、実行順など、テストに書かれていない入力でも変わります。書かれていないので、1回走らせただけでは見えません。同じコードで2回走らせ、結果を比べて初めて見えます。
- ・比べる相手は期待する出力ではなく、同じコードのもう1回の実行
- ・見つけた入力をテストの中で固定すると、その入力による揺れは消える
タイムゾーンで割れた、同じテスト
9時〜18時を営業中と判定する関数と、そのテストです。
// shop.mjs
export function isOpen(now = new Date()) {
const h = now.getHours(); // 実行した機械のタイムゾーンでの「時」
return h >= 9 && h < 18; // 9時〜18時が営業時間
}
// clock.test.mjs(抜粋)
test("営業中なら注文を受け付ける", () => {
console.log("TZ=", process.env.TZ, "local hour=", new Date().getHours(), "ISO=", new Date().toISOString());
assert.equal(isOpen(), true);
});
同じコードを TZ だけ変えて2回ずつ走らせました(node v24.21.0、macOS)。
### TZ=Asia/TokyoTZ= Asia/Tokyo local hour= 9 ISO= 2026-09-24T00:04:59.172Z✔ 営業中なら注文を受け付ける (1.119792ms)exit=0### TZ=Europe/LondonTZ= Europe/London local hour= 1 ISO= 2026-09-24T00:04:59.232Z✖ 営業中なら注文を受け付ける (1.648084ms)exit=1### TZ=Asia/TokyoTZ= Asia/Tokyo local hour= 9 ISO= 2026-09-24T00:04:59.293Z✔ 営業中なら注文を受け付ける (0.998125ms)exit=0### TZ=Europe/LondonTZ= Europe/London local hour= 1 ISO= 2026-09-24T00:04:59.353Z✖ 営業中なら注文を受け付ける (1.339375ms)exit=1
ISO 時刻はほぼ同じ瞬間です。東京では local hour= 9 で緑、ロンドンでは local hour= 1 で赤。結果を決めていたのは、テストのどこにも書かれていない「いつ、どこの機械で走らせたか」でした。Luo らの論文は Time の分類で a test may fail when the midnight changes in the UTC time zone と書きます。論文の例は UTC の日付の変わり目、この実演は現地の「時」で別物ですが、どちらもシステム時刻という書かれていない入力です。ただし論文で Time に分類されたのは161件中5件と少数で、この例は分かりやすさで選んでいます。
では時計を固定すれば済むか。MockTimers で「今」を 2026-09-24T03:00:00Z に固定した版と、さらに process.env.TZ = "Asia/Tokyo" も固定した版です。
### $ TZ=Asia/Tokyo node --test clock-mocked.test.mjsTZ= Asia/Tokyo local hour= 12 ISO= 2026-09-24T03:00:00.000Z✔ 営業中なら注文を受け付ける(時計を固定) (1.196084ms)### $ TZ=Europe/London node --test clock-mocked.test.mjsTZ= Europe/London local hour= 4 ISO= 2026-09-24T03:00:00.000Z✖ 営業中なら注文を受け付ける(時計を固定) (1.598083ms)### $ TZ=Europe/London node --test clock-pinned.test.mjsTZ= Asia/Tokyo local hour= 12 ISO= 2026-09-24T03:00:00.000Z✔ 営業中なら注文を受け付ける(時計とタイムゾーンを固定) (0.823459ms)
時計を固定しても、ロンドンでは local hour= 4 で赤のままです。MockTimers が固定するのは「どの瞬間か」までで、その瞬間を何時と読むかはタイムゾーンが決めます。タイムゾーンも固定した版は、東京・ロンドン2回ずつの4回とも緑でした(上は抜粋)。書かれていない入力は、1つとは限りません。
`isOpen()` のテストが東京の機械で緑になりました(時計もタイムゾーンも固定していない版)。この緑から言えることはどれですか。
161件の分類では、待ち方が最多だった
時刻は分かりやすい例ですが、多数派ではありません。Luo らは51のオープンソースプロジェクトから、flaky を直した可能性が高いコミット201件を調べ、原因を分類できた161件を数えました。
分類 件数 論文の表記 python3 で計算Async Wait 74 45% 45.96%Concurrency 32 20% 19.88%Test Order Dependency 19 12% 11.80%(3つの合計) 125 77% 77.64%Time 5 - 3.11%
最多は非同期の結果を待ち切らずに使うもの(Async Wait)、3位は the test outcome depends on the order in which the tests are run(Test Order Dependency)です。どちらも「どれだけ待ったか」「前に何が走ったか」という、テストに書かれていない入力です。この割合は2014年のこの標本のもので、flaky 一般の比率ではありません。
揺れは、放っておくと本物の失敗まで隠します。SWE at Google 11章の試算です。
If each test has even a 0.1% of failing when it should not, and you run 10,000 tests per day, you will be investigating 10 flakes per day.
Testing Blog は、その先で起きることを書いています。
It is quite common to ignore legitimate failures in flaky tests due to the high number of false-positives.
時々落ちるテストの赤は、無視されやすい。その中に本物の失敗が混ざります。flaky は「テストの問題」で片づけられるとは限りません。
100ミリ秒待つテストを、30回走らせる
序章『生成されたコードの読み方 — 「動く」と「正しい」の差』の対比例に、setTimeout(init, 100) で包んで「動くようになりました」と返す場面がありました。同じ形のテストです。
// loader.mjs: かかる時間は機械の混み具合で変わる(ここでは 0〜150ms の乱数で代用)
export function startLoader(state, delay = Math.random() * 150) {
setTimeout(() => { state.ready = true; }, delay);
}
// wait.test.mjs(抜粋): 100ms 待ってから確かめる
startLoader(state);
await sleep(100);
assert.equal(state.ready, true);
// wait-mocked.test.mjs(抜粋): 偽の時計を進める
t.mock.timers.enable({ apis: ["setTimeout"] });
startLoader(state, 150); // 最悪の遅さを入力として明示
t.mock.timers.tick(149);
assert.equal(state.ready, false);
t.mock.timers.tick(1);
assert.equal(state.ready, true);
wait.test.mjs: 30回中 pass=21 fail=9 並び=PFPPPFFPFFPPFPPPFPPPFPPPPPPPFPwait-mocked.test.mjs: 30回中 pass=30 fail=0 並び=PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPwait.test.mjs: 30回中 pass=19 fail=11 並び=PPPPPFPFPPPFPPPFFPPFPFPPFPFFPFwait-mocked.test.mjs: 30回中 pass=30 fail=0 並び=PPPPPPPPPPPPPPPPPPPPPPPPPPPPPP
遅れは0〜150msの一様乱数なので、100ms を超えて落ちる確率は約3分の1、30回なら約10回です。実測は9回と11回でした。コードは1文字も変わっていません。乱数は機械の混み具合の代用で、本物の遅れが乱数だという意味ではありません。
MockTimers 版は、待ち時間を「150ms」とテストに書き、時計をテストが進めます。書かれていなかった入力を、書かれた入力に変えたから、60回とも緑です。
待ち時間を延ばす直し方はどうでしょう。Luo らの Table 4 では、sleep の追加・変更による修正20件のうち、flaky を完全に除いたものは0件、減らしただけが20件でした。
非同期の完了を `sleep` で待つテストが時々落ちるので、待ち時間を延ばしたら落ちなくなりました。Luo らの2014年の研究に照らして、これをどう評価しますか。
100ミリ秒待つテスト
同じコードを30回走らせると、赤が9回または11回混ざりました。書かれていない入力は起動処理にかかった時間です。MockTimers で時計をテストが進めると、60回とも緑でした。
wait.test.mjs: 30回中 pass=21 fail=9 並び=PFPPPFFPFFPPFPPPFPPPFPPPPPPPFPwait-mocked.test.mjs: 30回中 pass=30 fail=0 並び=PPPPPPPPPPPPPPPPPPPPPPPPPPPPPP
共有の配列を使う2つのテスト
A は1件入れて1件、B はもう1件入れて2件を確かめます。宣言順では両方緑。--test-random-seed=1 で B が先に走ると、B は 1 !== 2、A も 2 !== 1 で落ちました。seed 1〜30 のうち B→A になったのは16個。beforeEach で配列を空に戻し、B が前提を自分で作る版は、seed=1 でも seed=12 でも緑でした。
$ node --test order.test.mjs✔ A: 商品を1つ入れると1件 (0.301125ms)✔ B: もう1つ入れると2件 (0.0495ms)$ node --test --test-random-seed=1 order.test.mjs✖ B: もう1つ入れると2件 (0.559084ms)✖ A: 商品を1つ入れると1件 (0.08225ms)
待ち時間のテストと、実行順のテスト。揺れた理由は違って見えますが、共通する構造は何でしょうか。
2回の実行のあいだで、コード以外に何が違っていたのかを探してください。
この章のまとめ
- Testing Blog の定義対象は "flaky" なテスト結果。同じコードで緑と赤の両方が出ること。
- 比べるのは同じコード同士。結果が違えば、差はコードの外にある。
- 実機では、タイムゾーンだけで東京は緑・ロンドンは赤。MockTimers で時計を固定してもタイムゾーンは残った。
- Luo らの161件では Async Wait が最多、Test Order Dependency が3位。分母は分類できたコミットで、一般の比率ではない。
- 待ち時間を延ばす修正は、Luo らの表で完全に除いたものが0件。直すとは、書かれていない入力をテストに書き込むこと。
自分の現場で確かめる
読んだだけでは身につきません。手元の環境で実際に見てみてください。
理解度チェック
読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。
Luo らの FSE'14 論文にある「Async Wait が 74 out of 161 (45%)」の分母 161 は何ですか。
用語集(4語)
- flaky
- Google Testing Blog(2016)は `a "flaky" test result` を、同じコードで成功と失敗の両方を示すものと定義する。定義されているのはテスト結果。
- 書かれていない入力
- テストの結果を左右するのに、テストのコードに書かれていないもの。時刻、タイムゾーン、待ち時間、実行順など。この教材での呼び方。
- 決定的 (deterministic)
- SWE at Google 13章の定義では、同じ版のシステムに対して常に同じ結果(常に成功か常に失敗)を返すテスト。
- MockTimers
- node:test の機能。setTimeout などと Date を偽の時計に置き換え、実時間を待たずに時計を進める。v23.1.0 で stable。タイムゾーンは固定しない。
出典(一次ソース)
- Google Testing Blog — John Micco, Flaky Tests at Google and How We Mitigate Them (2016-05-27) ↗
- Luo, Hariri, Eloussi, Marinov — An Empirical Analysis of Flaky Tests (FSE'14) ↗
- Software Engineering at Google — Chapter 11 Testing Overview(Case Study: Flaky Tests Are Expensive) ↗
- Software Engineering at Google — Chapter 13 Test Doubles ↗
- Node.js v24 Documentation — Test runner(Class: MockTimers / Randomizing tests execution order) ↗