作品データを 12 か所わざと壊して、回帰テストが赤くなるかを見た
Breaking the game data in 12 places on purpose to see whether the regression tests turn red
D20-4-T40-mutation-testing-data・技術・日本語入力でプレイする仕組み
検査が全部緑でも、その緑を信じてよいかは、緑を見ても分かりません。
だから今日、第一章の「女将は 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 時まで」。二つを聞いてからぶつけると、佐伯の顔色が変わります。『霧走町 流氷に眠る』の第一章をブラウザで遊んで確かめてください。プロフィールのリンクから。