kaedeasset記事 · Notes霧走町 流氷に眠る

作品データを 12 か所わざと壊して、回帰テストが赤くなるかを見た

Breaking the game data in 12 places on purpose to see whether the regression tests turn red

 D20-4-T40-mutation-testing-data・技術・日本語入力でプレイする仕組み

確かめ方mutation testingmutation scoreequivalent mutantfault injectionclaude自然言語処理

検査が全部緑でも、その緑を信じてよいかは、緑を見ても分かりません。
だから今日、第一章の「女将は 8 時前に帰ったと言った」まわりのデータを、1 か所ずつ 12 通りに壊しました。
回帰テストは 10 個を赤にし、2 個を見逃しました。
見逃した 1 つは、女将の台詞の時刻を書き換える変異でした。

この回の位置と、前後の回#

区切り: T34〜T49 確かめ方(16 回のうち 7 回目)
先に読むとよい回: T37(台本の中に期待を書く)・T39(言い換えても着地するか)
次に読む回: T41(判定の 4 値)

名前 — mutation testing#

正式名
mutation testing
関連
mutation analysis、mutation score、equivalent mutant、fault injection
日本語
ミューテーションテスト、変異テスト
初出
Richard A. DeMillo, Richard J. Lipton, Frederick G. Sayward, "Hints on Test Data Selection: Help for the Practicing Programmer", Computer 11(4), IEEE, 1978
概説
Yue Jia, Mark Harman, "An Analysis and Survey of the Development of Mutation Testing", IEEE TSE 37(5), 2011

mutation testing とは#

mutation testing(ミューテーションテスト)とは、テストの良し悪しを測る方法です。
被検査物(ふつうはソースコード)に、小さな誤りを 1 つだけ入れた版を作ります。
これを mutant(変異体)と呼びます。
テストを流して、どれかが落ちれば killed、全部通れば survived。

mutation score = killed / (全部の変異 − 等価な変異)

誤りの入れ方には型があり、mutation operator と呼びます。
比較演算子を替える(< を <= に)、定数を 1 ずらす、条件を反転する、文を消す、などです。

この方法は、二つの仮説の上に立っています。
DeMillo らの論文で述べられたものです。

  • competent programmer hypothesis … プログラマはほぼ正しいコードを書く。残る誤りは、小さな書き換えで直る程度のもの。
  • coupling effect … 単純な誤りを見つけるテストは、複雑な誤りもよく見つける。

だから、単純な誤りを 1 つずつ入れて捕まるかを見れば、テストの力が分かる、という理屈です。

よく知られた弱点は二つです。

  • 計算が重い。変異の数 × テストの全部、を流します。
  • equivalent mutant(等価な変異)。書き換えたのに振る舞いが変わらない変異は、どんなテストでも殺せません。それが等価かどうかは、一般には機械で決められません。

fault injection(故障の注入)は近い言葉で、動いている系に故障を入れて、系が耐えるか・検出するかを見ます。
mutation testing はテストの方を測り、fault injection は系の方を測る、と分けて使われることが多いです。

うちでの名前の混乱#

うちでは「変異テスト」が二つのものを指していました。
言い方を崩した入力を作る adv-sim mutate(T39 の metamorphic testing)と、データを壊して検査が赤くなるかを見る、この回のものです。
英語では別の名前です。
入力を変えて出力が変わらないことを見るのが metamorphic、被検査物を変えて検査が赤くなることを見るのが mutation です。
いまは、こちらだけを mutation testing と呼んでいます。

うちの被検査物はデータ#

うちのゲームの中身は、ほとんどが JSON です。
意図と言い換え、段の条件と立てるフラグ、場所と在席、焦点、手帳の既定文、閾値、台詞。
C++ のエンジンは、作品に固有の語を 1 つも持ちません。
だから、壊す相手はソースではなく作品データです。

作品の検査の入口(Game/adv/checks/extras.ps1)には、最初から壊したデータを渡す口があります。
冒頭の注記を写します。

-Data は変異テスト用。データを1か所わざと壊した複製を渡して、この検査が

ちゃんと赤くなるかを確かめる。緑を信じてよいかは、緑を見ても分からない。

param([switch]$Verbose, [string]$Data)

2026-08-06 に、この口で 4 か所を壊し、4 つとも赤になったことを確かめた記録があります。
今日はそれとは別に、12 か所を選びました。

今日のやり方#

作品データ(Game/adv と、共通の Game/common)を作業用の場所へ写し、写しの方だけを壊します。
worktree の作品データは触りません。
1 つ壊すたびに、毎周のループと同じ検査を流します。

  • lint(adv-loop と同じく、探りの台本も渡す)
  • golden/dev(176 問・合格ライン 172 問以上・進行の誤り 0)
  • runBy=loop の台本 27 本(目録の gates をそのまま渡す)
  • checks/extras.ps1 -Data <壊した写し>

流し終わったら、壊した所を元に戻して次へ進みます。
壊す前の写しで流すと、全部緑でした。
1 つにつき約 2 分です。

壊したのは、第一章の食い違いの所だけです。
佐伯「10 時まで番屋にいた」と、女将(ハル)「8 時前に帰った」。
二つを聞いてからぶつけると、佐伯の証言が崩れる、という流れです。

結果(10/29) — mutation testing#

M01 女将の証言の段が、旗(f_hal_saeki_8pm)を立てない → 赤(lint L2「この旗に届く道が無い」・台本 8 本・extras)
M02 突きつけの段が、女将の証言を前提にしない → 赤(extras の 1 項目だけ)
M03 突きつけの段が、「崩れた」の旗を立てない → 赤(lint L2・台本 8 本・extras)
M04 女将の証言の段が、話題の語を要求しない → 赤(lint L23「段の役割と挙動が食い違う」・台本 8 本・extras)
M05 採用線を 0.33 から 0.45 へ → 赤(台本 5 本・extras の雑談の項目)
M06 聞き返しの帯を消す(聞き返しの下限を採用線と同じに)→ 赤(extras の 1 項目だけ)
M07 番屋から女将の在席を消す → 赤(lint L14・golden/dev 149/176・台本 14 本・extras)
M08 「女将の話の裏をまだ取っていない」の焦点を消す → 赤(lint L27 だけ)
M09 女将の台詞の「8時前」を「9時前」に → 緑のまま
M10 固有名詞「番屋」の別名を空に → 赤(lint L27・台本 2 本・extras)
M11 女将の段の 0 番と 1 番を入れ替える → 緑のまま
M12 手帳の既定文「ハル「佐伯さんは8時前に帰った」」を消す → 赤(lint L48 だけ)

10 個が killed、2 個が survived。
mutation score は 10/12、83% です。
M11 を等価な変異とみなせば 10/11 です(下で書きます)。

reach(T36)は 1 回 2 分半かかるので、生き残った 2 つと、extras だけが拾った M02 にだけ流しました。
3 つとも PASS でした。
ただ M02 では、到達状態の数が 2,699,496 から 3,179,736 に増えました。
女将の証言の前に佐伯を崩せる道が、48 万状態ぶん増えたということです。
状態の数が増えたこと自体は、どの合格条件にも入っていません。

読み取れること#

一つ目、1 つの検査だけが拾った変異が 5 個あります。
M02 と M06 は extras の 1 項目、M08 と M12 は lint の 1 規則だけでした。
ループの台本 27 本も、golden/dev も、この 4 つでは緑でした。

M06 は、聞き返しの帯を丸ごと消す変異です。
惜しい入力に聞き返す、はうちの照合の大事な受け皿です(T17)。
それを消しても、台本 27 本と凍結した 176 問は全部通りました。
拾ったのは、応答の種類が全部 1 度ずつ出ることを見る台本(reason-tour)の 1 項目です。
「F1.YES・F1.NO・F1.CONFIRM|F1.WEAK が出ていない」と言って落ちました。
凍結した 176 問が聞き返しを見ていないのは、全部が 1 回で正解するように作ってあるからです(今日 REACH の +f1 は 0)。

M02 は、もっと気になります。
女将の証言を聞く前に、食い違いをぶつけて佐伯を崩せてしまう変異です。
推理の順番が壊れているのに、拾ったのは reason-tour の「受け流しの応答が出ない」でした。
順番そのものを見張る期待ではなく、別の目的の項目が、たまたま落ちたのです。

二つ目、M05 では golden/dev が 172/176 で、合格ラインちょうどで通りました。
採用線を 0.12 上げても、凍結した 176 問は 4 問しか落ちません。
合格ラインの 4 問の余裕が、ここで全部使われました。

三つ目、生き残った M09 は、本物の欠陥です。
女将が「9 時前に帰った」と言い、手帳には「8 時前に帰った」と残り、佐伯は「10 時まで番屋にいた」と言い張る。
話の筋が食い違います。
ところが、検査は台詞の文面を見ません。
!expect は着地の意図と段まで、lint は条件とフラグの形、golden は照合の結果だけを見ます。
文面の食い違いを見るのは、AI に本文を通読させる別の検査(通読テスト)で、毎周のループには入っていません。
既存の台詞を一字でも変えたら止める道具もありますが(T38 で書いた adv-authoring-guard)、それは「git の前の版との差」を見るもので、今日の写しは git の外にあるので通していません。毎周のループにも入っていません。

名前どおりでない所 — mutation testing#

一つ目、被検査物がコードではなくデータです。
mutation operator は、ソースコードの演算子の書き換えとして定義されることが多い。
うちの operator は、JSON の欄を消す、配列から 1 つ抜く、数を動かす、文字列を書き換える、です。
データ駆動の系では、こちらが「ほぼ正しく書かれたものの、小さな誤り」にあたります。
competent programmer hypothesis を、作品データを書く側に当てはめた形です。

二つ目、変異を機械で網羅していません。
12 個は、手で選びました。
本来の mutation testing は、operator を全部の場所に機械的に当て、数百・数千の変異を作ります。
今日の 83% は、選び方に強く依存します。
「第一章の食い違いの周りで、起こりそうな書き損じ」を選んだ、という条件つきの数字です。

三つ目、等価な変異を、ソースを読んで判定しました。
M11 は段の並びを入れ替えましたが、段は並びではなく役割で選ばれます。
AdvIntents.hpp の PickByTier から写します。

// 段位1: 成立している材料段のうち最後。1つでもあれば段位2 は見ない。
for (int i = 0; i < CountConfront; ++i)
{
if (OkConfront(i)) { Pick.Index = i; Pick.bConfront = true; }
}
if (Pick.Index >= 0) { return Pick; }
// 段位2: 条件なしの段(進行段・はぐらかし段)のうち最後。
for (int i = 0; i < CountPlain; ++i)
{
if (OkPlain(i)) { Pick.Index = i; }
}

女将の段は「材料段」と「はぐらかしの段」が 1 つずつなので、入れ替えても同じ段が選ばれます。
段の番号は変わりますが、台本と凍結した問いの期待は、段の名前か意図だけで書いてあります。
だから振る舞いは変わらず、等価に近い変異です。
完全に等価かは言えません(保存データが段を番号で持っていれば、古いセーブの読み込みで差が出ます。今日は確かめていません)。

弱点と、採らなかった案 — mutation testing#

  • 1 つに 2 分かかり、12 個で 25 分ほどでした。毎周には回せません。
  • 壊したのは第一章の一か所の周りだけです。第二章以降、手帳、表示、セーブは壊していません。
  • UE の画面とブラウザの画面は通していません。CLI の検査だけです。
  • ループの台本には、合格ラインを持たないものがあり、その !expect の失敗は戻り値に入りません(T37 の穴)。今日は別に数えました。M01・M03・M04・M07 で 6 から 75 に増えましたが、ループの赤には寄与していません。

やっていないことです。

  • C++ のエンジンへの変異。ソースを壊すとビルドが要ります。8 月に一度やり、元に戻したソースの更新時刻が古いままで、ビルドが変異版の実行ファイルを残した、という事故を踏んでいます(Copy-Item は更新時刻を保つ)。今日はデータだけにしました。
  • mutation testing を毎周の合否に入れる。生き残りを 0 にしろ、という条件にすると、生き残りを殺すための検査(たとえば台詞の一字一句を固定する検査)を足す方向へ押されます。それは無関係な変更で落ちる検査(Fragile Test。T55 で書きます)を増やすことになります。
  • M09 のような文面の食い違いを lint で見る。「8時前」のような事実を台詞と手帳と証言で突き合わせるには、事実の表が要ります。作るなら、台詞の事実を設定の側に一度だけ書いて、台詞はそこから検査する形です。今日はそこまで踏み込んでいません。

検索語#

mutation testing
mutation score equivalent mutant
competent programmer hypothesis coupling effect
DeMillo Lipton Sayward 1978

女将の「8 時前」と、佐伯の「10 時まで」。二つを聞いてからぶつけると、佐伯の顔色が変わります。『霧走町 流氷に眠る』の第一章をブラウザで遊んで確かめてください。プロフィールのリンクから。

#claude #自然言語処理