| name | oracle-differential |
| description | 期待値を1つずつ手で用意できないとき、正しいと信じられる別実装(参照実装・旧実装・別ライブラリ)と 出力を突き合わせる差分テスト(Differential Testing)を扱う。test-catalog の手法カタログの一部。 参照実装の独立性確認、入力空間の共通化、出力差ゼロの assert、浮動小数の許容誤差比較、 移行完了までの併走運用を検証したい、または割り当てたいときに使う。 通常は test-catalog スキルの索引経由で手法が選定された後にこのスキルを直接参照する。
|
| disable-model-invocation | true |
期待値を別実装で代替するテスト
期待値そのものを1つずつ手で書けないとき、別の信頼できる実装に「正しさの判定」を委ねるテスト群。
oracle 問題(期待値をどう用意するか)への解のうち、このファイルは「別の信頼できる実装と突き合わせる」手法(差分テスト)を扱う。
過去の自分の出力を基準に固める解(スナップショット、承認、仕様化)は oracle-past-output.md、
入力と出力の関係・抽象モデル・上流の形式手法に委ねる解(メタモルフィック、モデルベース、形式検証連携)は oracle-relational.md にまとめた。
各手法は 概要 / 目的といつ使うか / TypeScript example / 落とし穴 / 遂行手順(着手→完了)/ 完了チェック(もれ確認)/ 網羅の定義 の構成で示す。
目次
差分テスト(Differential Testing)
概要
同じ入力を信頼できる別実装(参照実装、旧版、別ライブラリ)にも流し、両者の出力が一致するかで判定する。
目的/いつ使う
期待値を自前で書けないが、正しいと信じられる別実装があるとき(最適化版と素朴版、自作と標準ライブラリ、移植元と移植先)に使う。
oracle 問題を「もう一つの実装」で解くのが本質。
参照実装が無い、あるいは両実装が同じ前提で同じ間違いをし得るなら、一致しても正しさの保証にならない。
TypeScript example
最適化版 fastSum を、素朴な参照実装と任意入力で突き合わせる(PBT と組み合わせると強い)。
import { describe, it, expect } from "vitest";
import fc from "fast-check";
import { fastSum } from "./fast-sum";
const refSum = (xs: number[]) => xs.reduce((a, b) => a + b, 0);
describe("fastSum: differential vs reference", () => {
it("agrees with the naive implementation on any int array", () => {
fc.assert(
fc.property(fc.array(fc.integer()), (xs) => {
expect(fastSum(xs)).toBe(refSum(xs));
}),
);
});
});
落とし穴
- 両実装が同じ仕様の穴(同じ丸め誤差、同じ未定義動作の解釈)を共有すると、一致してもバグは残る。独立性が命。
- 自己ループバックは参照実装にならない。同一コードベースの encode⇔decode 往復や自作クライアント⇔自作サーバの突き合わせは、両側が同じ間違いを共有して相殺し、仕様違反のまま緑になる(同じ仕様違反を両端に実装して外部実装と繋いだ瞬間に破綻した実例が複数ある)。外部仕様との互換が目的なら、片側を公式テストベクタか独立した出自の第三者実装に pin する。往復一致は「自分との一貫性」の検査であって「仕様準拠」の検査ではない。
- 浮動小数は厳密一致しないことがある。許容誤差での比較に切り替える。
遂行手順(着手→完了)
差分テストの本体は「旧実装(または参照実装)と新実装に同じ入力を流し、出力差がゼロであることを広い入力で確かめる」ことだ。次の順で進める。
- 参照実装を確保する:正しいと信じられる別実装を用意する。移植なら移植元、最適化なら素朴版、置き換えなら旧実装。参照が新実装と独立した出自であることを確認する(同じコードベースから派生していると同じ穴を共有する)。参照が無いなら差分テストは成立しない。
- 入力空間を共通化する:両実装に同一の入力を渡す経路を作る。型・前処理・前提状態を揃える。片方だけ正規化が掛かると差が出て偽陽性になる。PBT(
fast-check)の生成器で入力を広く取り、代表値だけでなく境界・極端値も踏ませる(generative-property.md)。
- 出力差ゼロを assert する:同入力に対する両出力を比較する。完全一致が前提なら
expect(new).toStrictEqual(ref)。浮動小数や順序揺れがあるなら許容誤差・正規化を比較側に入れる(-0/+0 や丸め差で flaky に落ちないよう)。
- 不一致を切り分ける:差分が出たら、新実装のバグか、参照側のバグか、比較条件(誤差・順序)の不備かを切り分ける。PBT の shrink で最小反例を得てから原因を特定する。
- 移行が終わるまで併走させる:置き換え目的なら、旧実装を消す前に差分テストを CI で併走させ、十分な入力で差ゼロを継続確認してから旧実装を撤去する。
完了チェック(もれ確認)
- 同入力が本当に同一か:両実装へ渡る入力が、前処理・型・状態まで含めて同一であることを確認する。片側だけの正規化が紛れると差分が偽陽性/偽陰性になる。
- 独立性があるか:参照実装と新実装が同じ未定義動作の解釈・同じ丸め・同じ仕様の穴を共有していないか。共有していれば「一致」は正しさを意味しない。出自を逆引きする。
- 比較条件が妥当か:浮動小数を厳密一致で比べて flaky になっていないか。許容誤差・順序正規化が入っているか。逆に誤差を緩めすぎて実バグを見逃していないか。
- 入力が広いか:代表値だけでなく境界・極端・異常入力まで PBT で踏んでいるか。seed を変えて複数回回しても差ゼロか(
../test-verify/SKILL.md の実行ゲートの flaky 確認)。
網羅の定義
- 網羅基準:信頼できる参照実装と、入力空間を代表する入力集合(PBT 併用で多数)で全件一致する。
- 網羅手順:
- 参照実装を用意する。
- 同じ入力を両者へ流す。
- 出力一致を assert し、PBT で入力を広く取る。
- 達成チェック:両実装が同じ穴を共有していないか(独立性)を確認する。浮動小数は許容誤差で比較する。
過去の出力を oracle に据える系(ゴールデン、承認、仕様化)は oracle-past-output.md にまとめた。
関係・抽象モデル・上流の形式手法を oracle に据える系(メタモルフィック、モデルベース、形式検証連携)は oracle-relational.md にまとめた。
ランダム入力を生成して性質や頑健性を叩く系(PBT、ファジング、コンビナトリアル)は generative-property.md にまとめた。