Henka:テストのためのテスト
テストが全部通れば、テストが通ったことはわかります。でも、コードが間違っていたらテストが失敗するかどうかはわかりません。Henka(変化)はそこを確かめます。コードをわざと小さく 1 か所ずつ壊し、そのたびにテストを実行して、どのテストも捕まえなかった変更をすべて一覧にします。行カバレッジ 94% の debuff のエフェクト用パッケージでは、Henka の 266 個の変更のうち 132 個がすべてのテストを通りました。その中には、全部のパーティクルを逆向きに飛ばす変更もあります(見つかったもの)。
npm install -D henka
npx henka✔ caught src/score.ts:3:15 hits * 2 → hits / 2
✖ missed src/score.ts:2:12 hits < 3 → hits <= 3
missed: no test failed, so nothing tests what these change. Add a test of the behaviour they control.
src/score.ts:2:12 hits < 3 → hits <= 3この見逃しは、< と <= で結果が変わる唯一の入力 combo(3) を、どのテストも呼んでいないということです。それを確かめるテストを書けば直ります。
作った理由
debuff のゲームでは、今はテストの大半をエージェントが書いています。テストを頼まれたエージェントは、通るテストを書きます。そうしたテストは全部の行を実行しますが、確かめていることは少なめです。プレイヤーが「最高速度に達する」ことを確かめるテストは、最高速度が 140 でも 600 でも通ります。カバレッジでは、こうしたテストと良いテストを区別できません。ミューテーションテストならできますし、その出力はエージェント向きです。見逃し 1 件ごとにファイル、行、変更が出るので、次のテストで何を確かめればよいかがわかります。研究でも同じ結果が出ています。生き残った変更をモデルに戻すと、もう一度頼み直すより良いテストが書けます(MutGen)。Meta のエンジニアは、変更をもとにテストを書く生成器のテストを 73% 採用しました(ACH)。
Henka は結果を mutants.out/ に書き出します。outcomes.json と、結果の種類ごとに 1 つのテキストファイルで、1 行に 1 つの変更です。missed.txt をエージェントに渡し、各行が担う振る舞いのテストを頼みましょう。
コマンド 1 つ、設定不要
Henka は、Vitest 5 と Vite 8 でテストしているプロジェクトならどこでも動きます。
- まず何も変えずにテストを 1 回実行し、各ソースファイルをどのテストファイルが読み込むかを記録します。
- Vite プラグインが、ファイルの読み込み時に変更後のコードへ差し替えます。ディスク上のファイルは変わらないので、エディタやウォッチャーが壊れかけのファイルを見ることはありません。
- 各変更では、そのファイルを読み込むテストファイルだけを、変更をまたいで開いたままの Vitest プロセスで実行し、最初の失敗で止めます。
- テストを止まらなくした変更は強制終了し、捕まえたものとして数えます。
--since mainを付けると、ブランチで変えた行だけを変更するので、プルリクエストでの実行は小さく済みます。
変えるのは算術、比較、論理、符号、数値リテラル、関数の本体全体で、文字列、型、import には触りません。一覧は Henka のページにあります。
ゲームのための変更
ゲームのテストには、ほかのテストにない悩みがあります。数値は変わるものだということです。デザイナーはジャンプの高さを何週間も調整します。その値を 140 に固定するテストは邪魔になります。そこで Keiko は、コードではなくゲームの調整値を変える Henka のアダプターを用意しています。
npx keiko mutate各調整用パラメータを範囲の両端まで動かし、テストが失敗することを期待します。Math.random() とブラウザのフレームレートを使うほかのエンジンのゲームでは、別のシードと 30 fps でも実行し、すべてのテストが通ることを期待します。そこで失敗するテストは brittle(もろい)で、運かフレームレートに頼っています。debuff のゲームは乱数を自分でシードし、固定ステップで動くので、この 2 つは当てはまりません。プラットフォーマーのテンプレートでは、パラメータの変更 16 個のうち 8 個が気づかれず、プレイヤーの空中加速はどちらの端でもテストされていませんでした。
どのテストランナーでも
Henka のコアは自分では何も実行しません。テストを実行して変更を適用するのはアダプターなので、同じレポートと結果の種類をどのランナーでも使えます。
import { mutate, writeOut } from "henka";
const report = await mutate({
name: "my-runner",
baseline: () => runMyTests(),
mutants: () => [{ id: "timeout=0", label: "request timeout 30 s → 0", expect: "noticed" }],
run: (mutant) => runMyTests({ apply: mutant.id }),
});
writeOut(report, "mutants.out");制限
npx henkaには Vitest が必要です。debuff のゲームは Keiko でテストするので、今はパラメータの変更だけを使えます。Keiko 経由のソースの変更は次に取り組みます。- 小さなパッケージで数百個の変更を作り、数分かかります。同じ負荷でほかのツールと比べる測定はまだしていないので、速さについては何も主張しません。
- テストからは違いが見えない変更もあります。そうした行には
// henka-skipを付けます。
Henka は MIT ライセンスで、ランタイム依存はありません。次のブランチで試してみてください。
npx henka --since main