45395 - シコウサクゴ -

機能をフラグで無効化すると旧仕様テストが『偽グリーン』になる:同じ事故を2度起こして学んだテスト設計

2026-06-22
AI駆動開発
AI駆動開発
Claude Code
テスト設計
kill-switch
偽陰性
Last updated:2026-06-22
15 Minutes
2877 Words

機能を削除する代わりに、設定フラグで無効化する——いわゆる kill-switch は、可逆で安全な廃止のやり方です。コードはそのまま残し、enable_feature=false のような設定1行で機能を止める。万一問題があれば設定を戻すだけで復活できます。

ところが、この kill-switch を使ったときに、思わぬところで足をすくわれました。**機能を無効化した瞬間、その機能を検証していた既存テストが「PASS したまま壊れる」**のです。テストは緑表示(成功)のまま通っているのに、本来検証すべきことを何も検証していない——いわゆる「偽グリーン」です。しかも同じ事故を、別の機能の廃止でもう一度繰り返しました。

本記事は、この2連発の事故から得た「廃止は積極的に証明するテストに書き換える」「設定値テストは値の断定でなくキーの読取可否・型を見る」という学びの記録です。

事の発端:無効化したら、テストがPASSしたまま空回りした

ある機能(定期的に成果物を生成するレポート機能を想定してください)を、kill-switch で無効化しました。エントリポイントの先頭にガードを置き、設定が enable_feature=false なら以降の処理を早期 skip して正常終了(exit 0)する、という作りです。

1
# エントリポイント先頭の kill-switch
2
if not config["enable_feature"]:
3
log("disabled by config (enable_feature=false), skipping")
4
return 0 # 以降のパイプラインに入らず正常終了

設定変更だけで機能が止まる、きれいな廃止です。ところがこの後、それまで通っていた既存テストの一部が、おかしな振る舞いを始めました。

ケース1:失敗系テストが exit 0 になって落ちる。 「メール送信が失敗したら exit 1 を返す」ことを検証するテストが、report_type="feature"(=無効化した機能)をハードコードしていました。すると kill-switch で先頭 skip され、メール送信に到達する前に exit 0 で返ってしまう。テストの期待は exit 1 なので FAIL します。

ケース2:成功系テストが「間違った理由で」PASSする。 一方、「処理が成功したら exit 0」「dry-run ならメールを送らない」といったテストは、通り続けました。ただし通っている理由が違います。本来は「処理が成功して exit 0」を確認したいのに、実際には「kill-switch で skip されて exit 0」になっている。メールを送らないのも「dry-run だから」ではなく「そもそも処理に入っていないから」です。

ケース2が厄介です。テストは PASS するので、誰も異常に気づきません。しかし検証しているつもりの挙動(成功経路・dry-run のメール抑止)は、一度も実行されていないのです。これが「偽グリーン」です。

なぜ問題か:「PASS」が検証の証明にならなくなる

テストの価値は「PASS なら、その挙動が正しいと信じられる」ことにあります。偽グリーンは、この信頼の根拠を崩します。

テストの種類無効化後の状態何が起きているか
失敗系(exit 1 を期待)🔴 FAILkill-switch の早期 skip で exit 0 になり期待と食い違う
成功系(exit 0 を期待)🟢 PASS(だが偽)「成功で 0」ではなく「skip で 0」。検証対象を素通り
設定値の断定(enable=True🔴 FAIL設定が false に変わったので断定が外れる

FAIL は気づけるのでまだマシです。本当に危険なのは、PASS したまま中身が空洞化したテストです。後からこの機能を復活させたり、別の改修を加えたりしたとき、このテストは「守ってくれている」つもりで、実は何も守っていません。

さらに、設定値を直接断定するテスト——たとえば「設定の enable_featureTrue である」と == True でアサートしているテスト——は、kill-switch で false に変えた瞬間に FAIL します。このテストの本来の意図は「設定にキーを足しても既存キーが読める」ことの確認だったのに、enable_featureを断定していたために、要件変更(無効化)のたびに壊れてしまう。値は本来 incidental(付随的)で、検証の本質ではなかったのです。

2度目で気づいた:これは「パターン」だ

最初の事故(ある機能の廃止)では、壊れたテストを個別に直して終わりにしました。ところが、別の似た機能を同じ kill-switch パターンで廃止したとき、まったく同じ構造の破損が再発したのです。

  • 廃止対象の機能の「実行前提」を断定していたテストが、早期 skip で FAIL する
  • 廃止対象を使っていた成功系テストが、偽グリーンで通り続ける
  • 設定値を断定していたテストが、設定変更で FAIL する

2度目で、これが単発のバグではなく横断的なパターンだと分かりました。「kill-switch で機能種別を廃止する変更では、その種別の実行を前提にした旧テストが必ず取り残されて壊れる」。一般化できる以上、再発防止の知見として形式知化する価値があります。

学び:廃止は「積極的に証明する」テストに書き換える

このケースを普遍化すると、kill-switch で機能を廃止するときのテスト設計の原則が見えてきます。

1つめ:廃止は「積極的に証明する」テストにする。

無効化した機能のテストは、放置すれば偽グリーンになります。だから、「この機能は廃止されたので、何も生成しない/何もしない」ことを能動的にアサートするテストに書き換えます。たとえば「この機能で成果物が0件であること」「exit 0 で skip ログが出ること」を確認する。こうすると、誤って機能を復活させたらテストが FAIL する——つまり回帰防止として機能します。「廃止された」という状態そのものをテストの検証対象に格上げするわけです。

1
# Before: 機能の実行を前提に成果物を期待(→ 偽グリーン or 早期 skip で FAIL)
2
def test_feature_produces_output():
3
rc = run(report_type="feature")
4
assert count_outputs("feature/") == 1 # 廃止後は 0 なので壊れる
5
6
# After: 廃止されたことを積極証明(誤って復活したら FAIL する)
7
def test_feature_is_deprecated():
8
rc = run(report_type="feature")
9
assert rc == 0
10
assert count_outputs("feature/") == 0 # 廃止=生成ゼロを証明
11
assert "disabled by config" in logs() # skip 経由であることも確認

2つめ:失敗系テストは「生きている経路」で検証する。

「失敗したら exit 1」のようなエラー処理を検証するテストが、廃止対象の機能を使っていたら、現役の機能種別に差し替えるべきです。廃止された種別では早期 skip されて、そもそもエラー処理に到達しません。検証したい挙動(エラー時の exit 1)を本当に exercise するには、その経路が生きている種別を使う必要があります。

3つめ:設定値テストは「値」でなく「読めるか・型」を見る。

設定値を == True で断定するテストは、要件変更のたびに壊れます。テストの意図が「キーが読めること」なら、値ではなくキーの読取可否や型を検証する方が保守的です。

1
# Before: 値を断定 → 設定変更で壊れる
2
assert config["enable_feature"] is True
3
4
# After: 安定 property(読取可能性・型)を検証 → 要件変更に強い
5
assert "enable_feature" in config # キーが読める
6
assert isinstance(config["enable_feature"], bool) # 型が正しい

設定のはビジネス要件で変わりますが、キーが存在し正しい型で読めることは安定した性質です。テストが検証すべきは後者であることが多いのです。

運用Tips:機能を無効化するPRのチェックリスト

実務に落とすと、kill-switch で機能を廃止する PR では、次を必ず確認します。

  • 廃止対象を使っているテストを全部洗い出す。 report_type="feature" のように廃止種別をハードコードしているテストは、FAIL するか偽グリーンになるかのどちらか。grep で機械的に列挙する。
  • PASS しているテストも疑う。 FAIL は気づけるが、偽グリーンは気づけない。廃止対象を使う成功系テストが「正しい理由で PASS しているか」を確認する。skip 経由で通っているなら積極証明に書き換える。
  • 設定値を断定しているテストを探す。 == True で設定値を断定しているテストは、無効化で壊れる。意図が「キーが読めること」なら読取可否・型の検証に直す。
  • 「廃止を証明するテスト」を1本足す。 廃止した機能が誤って復活したら FAIL するテストを置く。これが将来の回帰を止める最後の砦になります。

AI に「この機能を kill-switch で無効化して」と頼むと、エントリポイントのガードはきれいに入れてくれますが、取り残される旧テストの偽グリーンまでは面倒を見てくれないことが多いです。だから依頼には「廃止対象を使っているテストを全部洗い出し、偽グリーンになるものは廃止の積極証明に書き換えよ」と明示するのが効きます。

まとめ:無効化した機能のテストの「PASS」を信じない

kill-switch は安全な廃止の手段ですが、機能を無効化した瞬間、その機能を検証していたテストは PASS したまま空洞化します。早期 skip で通ってしまうテストは、検証しているつもりで何も検証していません。

今回の学びをまとめます。

  1. kill-switch で機能を無効化すると、旧テストは FAIL するか偽グリーンになる
  2. 偽グリーン(skip 経由で通る成功系テスト)は気づけないので最も危険
  3. 廃止は積極的に証明するテストに書き換える(誤って復活したら FAIL する)
  4. 失敗系テストは現役の経路で検証し直す
  5. 設定値テストは値の断定でなくキーの読取可否・型を見る

同じ事故を2度起こして分かったのは、これが単発のバグではなくパターンだということでした。機能を無効化する PR では「取り残された旧テストが偽グリーンになる」を前提に、テストを「現行挙動の証明」に転換する——それが、PASS したテストに裏切られないための設計です。

Article title:機能をフラグで無効化すると旧仕様テストが『偽グリーン』になる:同じ事故を2度起こして学んだテスト設計
Article author:45395
Release time:2026-06-22

記事へのご質問・ご感想をお聞かせください

フィードバックを送る