自動テストの実際 — 何を機械に見張らせるか
関数の戻り値は、もう主戦場ではありません。壊れるのは 音・レイアウト・読めない言語・公開物で、しかもエラーを出しません。 実際に事故を止めた検査を、コードごと全部書き出します。
従来の自動テストは、入力に対する出力を確かめるものでした。関数を呼び、期待値と比べる。 これは正解が1つに決まる領域ではよく機能します。
ところが個人がAIと作るものは、ほとんどが正解が1つに決まらない領域です。 「この音声は正しく読まれているか」「このレイアウトは重なっていないか」 「7言語のうち韓国語だけ壊れていないか」。従来の型では書けません。
そして最悪なのは、これらが失敗しても赤くならないことです。
テストが無いのではなく、テストできる形に落ちていなかった。
変わったのは2つです。ヘッドレスブラウザが安価に回せるようになったこと、 そして音声や画像を機械が判定できるようになったこと。 つまり「人が見ないと分からない」とされていた対象が、次々と数値に落ちるようになりました。
| 見たいもの | 以前 | いま |
|---|---|---|
| 音声が正しく読まれたか | 耳で聴く | 書き起こして音で照合 |
| 要素が重なっていないか | 目で見る | 座標を取って断言する |
| 公開物が正しいか | ブラウザで開く | ライブURLを叩いてハッシュ照合 |
| 翻訳が抜けていないか | 読める人に頼む | 件数と文字種で判定 |
生成した資産は、参照と実体を両方向で突き合わせます。片方向だと半分見逃します。
参照=237 実ファイル=237 欠落=0 孤児=0 破損疑い(<800B)=0
実績: 前作で161本の音声が無言のまま数週間動いていたのを、この検査で発見しました。
二言語同時字幕を入れたとき、「重なっていないこと」を目視で確認するのは毎回できません。 要素の矩形を取って、断言に変えます。
const ok = pg.evaluate(() => {
const a = document.querySelector('#tx').getBoundingClientRect();
const b = document.querySelector('#tx2').getBoundingClientRect();
return b.top >= a.bottom - 1; // 2つ目が1つ目を侵していない
});
同じ考え方で横スクロールの発生も一行で見られます。これはモバイル崩れの最頻出パターンです。
document.documentElement.scrollWidth > window.innerWidth + 2 // true なら崩れている
いちばん痛かった失敗です。多言語化で台本のデプロイを忘れたとき、 フォールバックが働いて全言語が完璧な日本語で表示されました。 ローカルのテストは全部通ります。手元には正しいファイルがあるからです。
# 公開されている実物と、手元が一致しているか
LOCAL=$(md5sum assets/voice/k2102619210.mp3 | cut -d' ' -f1)
curl -s https://example.com/assets/voice/k2102619210.mp3 | md5sum
反映待ちのポーリングは、対象に固有の文字列で行うこと。
ここでも一度失敗しました。「AIでしか作れない」で待っていたら、それは旧版にも含まれる語で、
反映前に反映済みと誤判定しました。待ち条件はid="newforms"のように新版だけが持つものにします。
クリア率の計測を入れたとき、テストのたびに本番の集計へ数字が入る状態になっていました。 外部への送信は遮断し、呼び出しだけ観測します。
pg.route("**gc.zgo.at/**", r => r.abort()); // 本物へ送らせない
pg.add_init_script(`window.__ev=[];
window.goatcounter={count:o=>window.__ev.push(o.path)};`);
// …操作したあと
assert "game/hyaku/clear" in pg.evaluate("window.__ev")
遮断を忘れると、本物のスクリプトが読み込まれてスタブを上書きし、観測が空になります。 実際にそれで一度、原因を見誤りました。
書き起こしは音を漢字に戻すので、読みの間違いは文字比較では原理的に検出できません。 漢字トークンを禁止して、仮名で出させます。
kanji = re.compile(r'[一-鿿]')
suppress = [-1] + [i for i in range(tok.get_vocab_size()) if kanji.search(tok.decode([i]))]
model.transcribe(path, language="ja", suppress_tokens=suppress,
initial_prompt="ひらがなだけでかきます。")
自動判定は間違えます。長文で仮名を強制すると、デコードが破綻して 一致率0.09のような異常値を出すことがある。音声そのものは正常なのにです。
切り分けは、条件を変えて3回聞き、多数決を取る。それと物理量の確認です。
尺 24.0s / 想定 21.7s → 比 1.11 # 音声は存在する
音量プロファイル: # # # # # # . # # # # 途中で無音になっていない
→ 一致率の異常は、書き起こし側の破綻と判断
テストが失敗を報告したとき、テスト自身を疑う手順を持っておくのが実務では重要です。
大きなHTMLのブロック移動は、閉じタグを1つ落とすだけでページが崩壊します。 書き込む前に収支を数えて、合わなければ書き込まない。
before = counts(src) # div/section/a/p の開き・閉じ
src = 変換(src)
after = counts(src)
assert all(after[t][0] == after[t][1] for t in after), "タグ収支が合わない"
open(path, "w").write(src) # 通ったときだけ書く
実績: 過去に閉じタグ2つを落としてページ幅が7,972pxに爆発した事故があり、以来この順序を守っています。
着手前に、何をもって終わりとするかをコマンドの出力で決めます。主観の「できました」を許さない。
| 工程 | 完了の観測 |
|---|---|
| 台本 | 実プレイで最終話に到達できる |
| 音声 | 欠落0 / 孤児0 + 読み検証PASS |
| 多言語 | 各言語の要素数が一致(238件) |
| 公開 | ライブURLを叩いて期待の中身が返る |
全部を毎回回すと、走らせなくなります。安いものを常に、高いものを条件付きで。
| 症状 | 原因 | 抜け方 |
|---|---|---|
| スクリプトがJS構文エラーで動かない | シェル→Python→JSとエスケープを三重に通した | 正規表現をJS側に書かない。innerTextだけ取ってPythonで解析する |
| ヒアドキュメントでファイルが壊れる | \bや\tがエスケープとして解釈された |
大きな差し込みは別ファイルに書いてから読み込む |
| ブラウザ自動化が接続できない | 既定プロファイルではデバッグポートが開かない/既に起動中だとフラグが無視される | 自動化専用プロファイルを分ける。普段のブラウザを閉じずに済む |
| 選択肢のクリックが効かない | アニメーション中で操作可能判定に入らない | JSの.click()で直接呼ぶ |
この3つを回すだけの巡回テストは、実際こう出ます。
/index.html err=0 PC溢=False SP溢=False
/method/ err=0 PC溢=False SP溢=False
/method/hinshitsu-kenmon.html err=0 PC溢=False SP溢=False
内部リンク切れ: なし
判定: ALL OK
自動テストの目的は、正しさの証明ではありません。「気づかないまま出す」ことを防ぐことです。
だから見張る対象は、コードではなく読者が実際に触れるものに置きます。