| name | quality-assurance |
| description | Flix プロジェクトのテスト設計指針。テストを設計するとき、新しい Scene ファイルやモジュールが作られたとき、Game.flix などゲームロジックを編集したとき、テストの品質が下がっていると感じたときに参照する |
| user-invocable | false |
Flix テスト設計指針
このプロジェクトのテストで大事にしていること。@Test を書く・直すときは必ずこの指針に沿うこと。
(Flix 構文・コーディングスタイル全般は flix-docs skill を参照)
テストファイル先行(新しいモジュールができたらまず考える)
新しいシーンファイルやモジュールを作ったら、まず対応するテストファイルを作ることを検討する。
- 「どう使われるか」「どんな性質を満たすか」を先に書くと、設計の歪みが早く見える
- テスト先行が難しい場合でも、モジュール完成と同じコミット範囲でテストを置く
- 既存モジュールに公開関数を足したら、そのコミットで
@Test も足す
価値のあるテストを書く(自明なテストは書かない)
「値をセットしてその値を取り出して同じか確認」のような、型シグネチャから自明に従う性質のテストは書かない。
読んだ人が その関数の振る舞いや性質を理解できる テストを優先する。
- ❌ 自明なテスト(書かない)
setX(v) |> getX == v — フィールドアクセサの恒等性
- 委譲だけしている関数のラッパテスト
- コンパイラが既に保証している型の確認
- ✅ 価値のあるテスト(書く)
- 複数の操作を組み合わせたときの帰結(重力で落ちる、衝突で状態が変わる、難易度に応じてパラメータが線形補間される)
- 境界条件(端っこで止まる、上限で頭打ちになる、空入力で何もしない)
- 状態遷移の順序依存(A → B はできるが A → C はできない)
- 不変条件(処理後にこの値は範囲内に収まる、コリジョンが二重登録されない)
- 回帰の温床(過去にバグった条件、仕様の根拠が外部にある数式)
判断基準: テストを読んだ人が「この関数はこういう性質を持つのか」と理解できれば価値あり。
「コードを別の言葉で書き直しただけ」になっていたら不要。
テストの目的と責務
- 対象モジュールの公開 API・初期化関数を経由してテストする。テスト用に内部状態を手で組み立てない
- 入力データは「本物の構築関数」を通したものを起点にする
- 構造そのものを検証するテストは、ローダ/ビルダーを直接呼ぶ
- 責務外の値を一緒に検証しない。物理のテストでアニメーションを見ない、構造のテストで状態遷移を見ない
- テスト名は「対象 + 振る舞い」を表す(例:
testXxxFallsWhenYyy, testXxxClampedAtBoundary)
1 テスト 1 アサート(必須)
複数の値を確認したいときは タプルや List にまとめて 1 回の assertEq で比較する。
複数の assertEq を並べない(失敗時にどれが落ちたか追いづらく、最初の失敗で残りが評価されない)。
// NG: 複数 assert
Assert.assertEq(expected = 4.0, r#x);
Assert.assertEq(expected = 6.0, r#y)
// OK: タプル化
Assert.assertEq(expected = (4.0, 6.0), (r#x, r#y))
例外: 純数値ユーティリティで各成分を独立に確認することが意味を持つ場合のみ短い連続 assert を許容する。シーン/状態系のテストでは必ずタプル化する。
分岐を書かない/bug! の使い所
- テスト本体に
if / 多分岐の match を書かない。期待される 1 ケースのみマッチさせる
match で「絶対に来てはいけない分岐」は bug!("...") にする。スキップや空 assert はしない
- 期待ケースで取り出した値そのものをアサートに使う形に書く
// OK: 期待形にだけマッチし、他は bug!
match lookup(key, container) {
case Some(value) =>
Assert.assertEq(expected = ..., extract(value))
case _ => bug!("expected entry not found")
}
Effect ハンドラによるモック
副作用は effect 多相のヘルパー関数 でモックする。テスト本体に run f() with handler X { ... } を直書きしない。
共通モックは 集約用テストファイル に置き、新しいテストでは 再利用する。新しいモックが必要なら、既存ヘルパーと同じパターンで集約ファイルに追加する。
// 必ず effect 多相で書く(呼び出し元の他の効果を消さない)
def withMockClock(f: Unit -> a \ ef + Clock): a \ ef =
run f() with handler Clock {
def currentTime(_, k) = k(0i64)
}
ポイント:
- シグネチャを
Unit -> a \ ef + 対象Effect にして、他の effect は ef で透過させる
- ハンドラは「決まった値を返す」「副作用を何もしない」などテストにとって都合のよい挙動にする
- 複数モックが必要なら 入れ子にせず合成する(
withMockA(() -> withMockB(() -> ...)))
- モック自体に複数 effect が混ざる場合は 1 つの
run に複数 with を連結する(with handler X { ... } with handler Y { ... })
共通ヘルパー
複数のテストファイルから使う操作は 集約用テストファイルに 1 か所 にまとめる。重複定義しない。
代表例:
- 初期化済みドメインオブジェクトを返すファクトリ(
testXxx(): Xxx \ ...)
- ドメイン型を
assertEq 用のタプルに変換するアダプタ(xxxToTuple)
- 「特定の操作を 1 ステップ進める」薄いラッパ(
runStep(state): State)
1 ファイル内のローカル便宜関数はそのファイル内に置く。共通化と局所化の境界は「他ファイルから使うか」で判断する。
テストにロジックを書かない(ヘルパーも含む)
テスト本体・ヘルパーともに ロジックは最小限にとどめる。
テストにロジックがあると、そのロジックが正しいことを別のテストで確かめないといけなくなる(=テストのためのテスト)。
- ❌ 危険のサイン — テストやヘルパーに以下が出てきたら一度立ち止まる
List.fold / List.foldLeft で期待値を組み立てる
List.map / List.filter で入力データを加工する
if で期待値を出し分ける
- 自前の再帰関数で結果を計算する
- ループで複数ステップ進めて値を集める
- ✅ あるべき姿
- 入力は 直接書いた具体値、期待値も 直接書いた具体値
- 「N ステップ進める」が必要なら、ステップ数だけ書き並べる か、対象 API 側に「N 回進める関数」を用意する
- ヘルパーは 副作用のモック と 値の取り出し/タプル変換 に限る
判断基準: そのヘルパーが間違っていたらテストが嘘になるか? Yes ならロジックが多すぎる。
ヘルパーは「呼び出し方をそろえる」「effect を剥がす」だけにし、計算は対象モジュール側の関数に押し戻す。
コメントは平易な言葉で
専門用語・カタカナ語・略語は避け、そのドメインを知らない人にも伝わる平易な言葉で書く。
- ❌ 「アキュムレートした値を比較する」
- ✅ 「足し合わせた値を比較する」
- ❌ 「インテグレートして 0.5s 後のポジションを assert」
- ✅ 「0.5 秒分の動きを進めた後の位置を確認する」
- ❌ 「クランプ範囲をバリデートする」
- ✅ 「上限・下限の範囲に収まっているか確認する」
専門用語が必要なときは 言い換えを併記する(例: 「正規化(長さを 1 にそろえる)」)。
グルーピングと意図コメント
Flix には describe がないので、大きめの ASCII コメントでセクションを区切る。
// ========================================================================
// 重力: 物体は重力で下方向に落下する
// ========================================================================
/// 0.5 秒分の動きを進めた後、y が 400 より大きくなる
/// (重力が下向きに効いていることを位置の変化で確認する)
@Test
def testFallsUnderGravity(): Unit \ {Assert, Fs.FileRead} = ...
- 各
@Test の上にドキュメントコメント(///)で 何を確認しているか + 重要な数値の根拠 を平易に書く
- 複雑な座標計算・段階的な処理・難易度カーブなどは アスキーアート で図示する
- 数値の出どころ(係数・線形補間・モック値)が一画面で読めるようにする
- セクションの並び順は「初期状態 → 単発の操作 → 操作の組み合わせ → 状態遷移」の順にする
テストデータは区切りの良い数値で
入力値・期待値は 人間が暗算で追える区切りのよい数 を選ぶ。半端な実測値をそのまま貼り付けない。
- ❌ 「位置 (112, 331) から (234, 521) に動く」 — 数値の意味が読み取れない
- ✅ 「位置 (0, 0) から (50, 100) に動く」 — 速度 50/100 で 1 秒進めた、と一目でわかる
選び方の指針:
- 原点・整数・10 や 100 の倍数を優先する(
0, 10, 50, 100, 0.5, 1.0)
- 期待値が 入力から手計算で導けるように 入力を選ぶ(重力 10 × 1 秒 = 速度 10、など)
- 既存定数(重力加速度、フレームレート)は固定値として受け入れ、それと 掛け算しても綺麗な数になる時間幅 をテストで選ぶ(0.1 秒、0.5 秒、1.0 秒)
- どうしても半端な数になる場合(線形補間の中間など)は、根拠の数式をコメントに書く
/// 速度 100 で 0.5 秒進めると x = 50 になる(100 × 0.5)
@Test
def testMovesAtConstantVelocity(): Unit \ Assert =
let after = move({x = 0.0, y = 0.0}, {x = 100.0, y = 0.0}, 0.5);
Assert.assertEq(expected = (50.0, 0.0), (after#x, after#y))
浮動小数点比較
- 等価比較できる値(決まった結果が出る計算、固定値)は
assertEq で OK
- 計算誤差が出る値(長さを 1 にそろえる処理、三角関数、連続した動きの積み重ね)は
Float64.abs(actual - expected) < epsilon で assertTrue
- epsilon は意味に応じて選ぶ(位置: 0.001、長さ: 0.01 など)
@Test シグネチャ
- 戻り値は 必ず
Unit
Assert エフェクトは必須。ファイル読込が必要なら Fs.FileRead も付ける
- DefaultHandler に任せて手書き handler は書かない(
flix-docs の優先度ルール)
@Test
def testFoo(): Unit \ {Assert, Fs.FileRead} = ...
チェックリスト(テストを書き終えたら確認)