本文へスキップ

Henka:テストのためのテスト ​

テストが全部通れば、テストが通ったことはわかります。でも、コードが間違っていたらテストが失敗するかどうかはわかりません。Henka(変化)はそこを確かめます。コードをわざと小さく 1 か所ずつ壊し、そのたびにテストを実行して、どのテストも捕まえなかった変更をすべて一覧にします。行カバレッジ 94% の debuff のエフェクト用パッケージでは、Henka の 266 個の変更のうち 132 個がすべてのテストを通りました。その中には、全部のパーティクルを逆向きに飛ばす変更もあります(見つかったもの)。

bash
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 のアダプターを用意しています。

bash
npx keiko mutate

各調整用パラメータを範囲の両端まで動かし、テストが失敗することを期待します。Math.random() とブラウザのフレームレートを使うほかのエンジンのゲームでは、別のシードと 30 fps でも実行し、すべてのテストが通ることを期待します。そこで失敗するテストは brittle(もろい)で、運かフレームレートに頼っています。debuff のゲームは乱数を自分でシードし、固定ステップで動くので、この 2 つは当てはまりません。プラットフォーマーのテンプレートでは、パラメータの変更 16 個のうち 8 個が気づかれず、プレイヤーの空中加速はどちらの端でもテストされていませんでした。

どのテストランナーでも ​

Henka のコアは自分では何も実行しません。テストを実行して変更を適用するのはアダプターなので、同じレポートと結果の種類をどのランナーでも使えます。

ts
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 ライセンスで、ランタイム依存はありません。次のブランチで試してみてください。

bash
npx henka --since main