機能を削除する代わりに、設定フラグで無効化する——いわゆる kill-switch は、可逆で安全な廃止のやり方です。コードはそのまま残し、enable_feature=false のような設定1行で機能を止める。万一問題があれば設定を戻すだけで復活できます。
ところが、この kill-switch を使ったときに、思わぬところで足をすくわれました。**機能を無効化した瞬間、その機能を検証していた既存テストが「PASS したまま壊れる」**のです。テストは緑表示(成功)のまま通っているのに、本来検証すべきことを何も検証していない——いわゆる「偽グリーン」です。しかも同じ事故を、別の機能の廃止でもう一度繰り返しました。
本記事は、この2連発の事故から得た「廃止は積極的に証明するテストに書き換える」「設定値テストは値の断定でなくキーの読取可否・型を見る」という学びの記録です。
事の発端:無効化したら、テストがPASSしたまま空回りした
ある機能(定期的に成果物を生成するレポート機能を想定してください)を、kill-switch で無効化しました。エントリポイントの先頭にガードを置き、設定が enable_feature=false なら以降の処理を早期 skip して正常終了(exit 0)する、という作りです。
1# エントリポイント先頭の kill-switch2if 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 を期待) | 🔴 FAIL | kill-switch の早期 skip で exit 0 になり期待と食い違う |
| 成功系(exit 0 を期待) | 🟢 PASS(だが偽) | 「成功で 0」ではなく「skip で 0」。検証対象を素通り |
設定値の断定(enable=True) | 🔴 FAIL | 設定が false に変わったので断定が外れる |
FAIL は気づけるのでまだマシです。本当に危険なのは、PASS したまま中身が空洞化したテストです。後からこの機能を復活させたり、別の改修を加えたりしたとき、このテストは「守ってくれている」つもりで、実は何も守っていません。
さらに、設定値を直接断定するテスト——たとえば「設定の enable_feature は True である」と == 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)2def test_feature_produces_output():3 rc = run(report_type="feature")4 assert count_outputs("feature/") == 1 # 廃止後は 0 なので壊れる5
6# After: 廃止されたことを積極証明(誤って復活したら FAIL する)7def test_feature_is_deprecated():8 rc = run(report_type="feature")9 assert rc == 010 assert count_outputs("feature/") == 0 # 廃止=生成ゼロを証明11 assert "disabled by config" in logs() # skip 経由であることも確認2つめ:失敗系テストは「生きている経路」で検証する。
「失敗したら exit 1」のようなエラー処理を検証するテストが、廃止対象の機能を使っていたら、現役の機能種別に差し替えるべきです。廃止された種別では早期 skip されて、そもそもエラー処理に到達しません。検証したい挙動(エラー時の exit 1)を本当に exercise するには、その経路が生きている種別を使う必要があります。
3つめ:設定値テストは「値」でなく「読めるか・型」を見る。
設定値を == True で断定するテストは、要件変更のたびに壊れます。テストの意図が「キーが読めること」なら、値ではなくキーの読取可否や型を検証する方が保守的です。
1# Before: 値を断定 → 設定変更で壊れる2assert config["enable_feature"] is True3
4# After: 安定 property(読取可能性・型)を検証 → 要件変更に強い5assert "enable_feature" in config # キーが読める6assert 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 で通ってしまうテストは、検証しているつもりで何も検証していません。
今回の学びをまとめます。
- kill-switch で機能を無効化すると、旧テストは FAIL するか偽グリーンになる
- 偽グリーン(skip 経由で通る成功系テスト)は気づけないので最も危険
- 廃止は積極的に証明するテストに書き換える(誤って復活したら FAIL する)
- 失敗系テストは現役の経路で検証し直す
- 設定値テストは値の断定でなくキーの読取可否・型を見る
同じ事故を2度起こして分かったのは、これが単発のバグではなくパターンだということでした。機能を無効化する PR では「取り残された旧テストが偽グリーンになる」を前提に、テストを「現行挙動の証明」に転換する——それが、PASS したテストに裏切られないための設計です。