| name | generative-property |
| description | 期待値(oracle)を用意しにくい対象を、入力全体に成り立つ性質やパラメータの組合せで 縛る生成テスト(プロパティベーステスト/PBT、コンビナトリアルテスト)を扱う。 test-catalog の手法カタログの一部。往復(round-trip)・不変条件・メタモルフィック関係 ・既知オラクル一致による性質の列挙、pairwise/t-way covering array による組合せ縮約を 検証したい、または割り当てたいときに使う。通常は test-catalog スキルの索引経由で 手法が選定された後にこのスキルを直接参照する。
|
| disable-model-invocation | true |
性質と組合せで縛る生成テスト(PBT / コンビナトリアル)
入力を機械が生成する手法のうち、「どんな入力でも成り立つ関係(性質)」や「パラメータの組合せを小さな集合で覆うこと」で正しさを縛る系をまとめる。
頑健性(不正入力で壊れないこと)を叩くファジング系は generative-fuzzing.md に分けた。
古典のブラックボックス技法が「この入力でこの出力」を1つずつ固定するのに対し、ここでは入力例を列挙せず、入力全体に対する性質(PBT)か、パラメータ空間を覆うcovering array(コンビナトリアル)でまとめて縛る。
各項目は「概要」「目的/いつ使う」「TypeScript example」「落とし穴」「遂行手順(着手→完了)」「完了チェック(もれ確認)」「網羅設計での効かせ方」の構成で示す。
目次
プロパティベーステスト(Property-Based Testing)
概要
個別の入力例ではなく、入力全体に対して成り立つべき性質(不変条件)を宣言し、ライブラリが多数のランダム入力を生成して反例を探す。
目的/いつ使う
「逆関数の往復で元に戻る」「可換」「結果は常にソート済み」のように、出力そのものより入力と出力の関係を言えるときに使う。
反例が見つかると自動で最小化(shrink)され、最小の壊れる入力が手に入る。
逆に成り立つ性質が「実装をそのまま書き写しただけ」になるなら oracle になっておらず、無価値なので使わない。
TypeScript example
JSON エンコード/デコードの往復(round-trip)を性質として叩く。
import { describe, it, expect } from "vitest";
import fc from "fast-check";
import { encode, decode } from "./codec";
describe("codec: properties", () => {
it("round-trips any value (decode . encode = id)", () => {
fc.assert(
fc.property(fc.jsonValue(), (value) => {
expect(decode(encode(value))).toStrictEqual(value);
}),
{ numRuns: 1000, seed: 42 },
);
});
});
落とし穴
- 生成器が狭いと(正の整数だけ等)肝心の境界値や異常値を踏まず、緑なのに穴が残る。
fc.jsonValue のように広い生成器を選ぶ。
- 性質が実装の写し(同じロジックで期待値を作る)になると、両方同時に間違っても通る。
- 性質は正しいのに比較の等価判定が落とし穴になる。広い生成器は
-0、+0、NaN、極小差の浮動小数を踏むので、toBe(Object.is)は -0 と +0 を別物と判定し、NaN === NaN は常に偽になる。数値の性質では === や許容誤差つき比較を選び、何を等価とみなすかを性質の一部として決める。手法が正しくても等価判定がずれると flaky に落ちる。
遂行手順(着手→完了)
PBT の本体は「対象を縛る性質を列挙し、各性質を反例の出る形のアサーションに落とす」ことだ。次の順で進める。
- 性質の種類で洗い出す:対象に成り立つ関係を、4つの型をテンプレートにして挙げる。1つも思いつかないなら PBT は向いていない(例ベースに戻す)。
- 不変条件(invariant):出力が常に満たす制約。
sort の結果は常に昇順、長さは入力と同じ。
- round-trip(往復):
decode(encode(x)) === x、parse(format(x)) === x。エンコード/シリアライズ境界の定番。
- メタモルフィック関係:入力を変換したときの出力の関係。
sort(reverse(xs)) と sort(xs) は等しい、abs(-x) === abs(x)。oracle が無い対象で特に効く。
- 既知のオラクルとの一致:遅いが自明に正しい参照実装(brute-force)と結果が一致する。
- 各性質が実装の写しでないか確かめる:期待値を「テスト対象と同じロジック」で組み立てていないか。同じなら両方同時に間違っても緑になり oracle として無価値。独立した根拠(別アルゴリズム・数学的定義)で期待を作る。
- 生成器を入力空間に合わせて広く取る:
fc.integer()(負・0・極値を含む)、fc.jsonValue()、fc.string() など。狭い生成器(正の整数だけ等)は境界を踏まず穴を残す。前提条件があるなら fc.pre() か .filter() で絞る(絞りすぎると生成効率が落ちるので生成器側で形を作る方を優先)。
- 等価判定を性質の一部として決める:数値なら
-0/+0/NaN・浮動小数の扱いを toBe(Object.is)/===/許容誤差から選ぶ。オブジェクトは toStrictEqual。ここを決めないと手法が正しくても flaky に落ちる(落とし穴参照)。
- 試行回数と seed を固定する:
{ numRuns: N, seed: S } を明示し、打ち切り(何回試したら緑とみなすか)と再現性を確定させる。CI で回すなら numRuns を環境で上げ下げできるようにする。
- 反例の最小化(shrink)を記録に残す:落ちたら fast-check が出す最小反例と seed を回帰テスト(
fc.examples か通常の例ベーステスト)に固定し、同じ入力で恒久的に縛る。shrink された最小例を捨てると次回ゼロから探索になる。
完了チェック(もれ確認)
- 性質が型ごとに網羅されているか:手順1の4型(不変・round-trip・メタモルフィック・既知オラクル)のうち、対象に当てはまるものを全部 property 化したか。該当する型を1つでも書き落としていれば穴。
- 各 property に seed と numRuns が付いているか:
grep -n "fc.assert" <test> の各ヒットに seed/numRuns 指定があるか。無ければ再現不能・打ち切り基準なし。
- 生成器が境界と異常値を踏むか:狭い生成器(
fc.nat() のみ等)で済ませていないか。負・0・極値・空・巨大入力を踏む生成器になっているか目視する。
- 性質が独立 oracle になっているか:期待値の組み立てがテスト対象の写しになっていないか。同じロジックなら oracle 失格。
- 最小反例が回帰に固定されたか:過去に落ちた最小反例(shrink 結果)が、seed 固定 or 例ベースで恒久テストに残っているか。残っていなければ同じバグが再発する。
網羅設計での効かせ方
PBT の網羅対象は「対象の正しさを縛る性質の集合」であり、入力例の集合ではない。
ゆえに網羅は「成り立つべき性質をすべて property 化し、各 property を十分広い生成器で多数試行して反例ゼロ」で定義する。
性質の列挙が網羅の上限を決めるので、手順1の4型で漏れなく挙げることが網羅の中核になる(挙げ損ねた性質は永遠にテストされない)。
生成器の広さが、列挙した性質を入力空間のどこまで踏ませるかを決める。狭い生成器は性質が正しくても境界の穴を残す。
例ベースの同値分割・境界値(blackbox-partition.md 系)と競合せず補完関係にある。例ベースで代表点を固定し、PBT で「その代表点の間も含めた全域で性質が保たれるか」を覆う。
配分の原則は、関係で言える正しさは PBT に寄せ、特定の入出力対応(回帰や仕様の具体例)は例ベースに残すことだ。
コンビナトリアルテスト(Combinatorial Testing)
概要
複数のパラメータが取り得る値の全組合せではなく、任意の t 個のパラメータの値の組合せを必ず一度は含む小さな集合(t-way / covering array)を自動生成する。
目的/いつ使う
設定フラグ、対応環境、入力カテゴリなど独立パラメータが多く、全組合せが爆発するときに使う。
欠陥の多くは少数パラメータの相互作用で起きるという経験則により、2-way(pairwise)で大半を、3-way で更に深い相互作用を、現実的なケース数で押さえる。
パラメータ間に強い依存(ある値が別の値を無効化する)があるなら制約を入れずに使うと無効ケースを量産するので、制約対応の生成器が要る。
TypeScript example
@fast-check/vitest などにある pairwise 生成を使い、3パラメータの 2-way 組合せを回す。
import { describe, it, expect } from "vitest";
import { pairwise } from "./pairwise";
const os = ["linux", "mac", "win"] as const;
const node = ["18", "20", "22"] as const;
const mode = ["dev", "prod"] as const;
describe("build matrix: 2-way coverage", () => {
it.each(pairwise(os, node, mode))("builds on %s/node%s/%s", (o, n, m) => {
expect(build({ os: o, node: n, mode: m }).ok).toBe(true);
});
});
落とし穴
- t を上げるほどケース数が急増する。まず 2-way で測り、相互作用バグが残る箇所だけ 3-way に上げる。
- 無効な値の組合せを除外する制約を入れないと、生成されたケースの多くが意味を成さない。
遂行手順(着手→完了)
コンビナトリアルの本体は「パラメータ空間を定義し、無効組合せを制約で削り、covering array をツールに生成させる」ことだ。各ステップにツールと検証が紐づく。
- パラメータと値域を列挙する:テスト対象が受ける独立な軸を全部挙げ、各軸の取り得る値を有限集合に落とす。連続値は同値クラス(
blackbox-partition.md の同値分割)で代表値へ離散化する(例:タイムアウト → 0 / 正常 / 最大 / 超過)。軸の挙げ漏れはそのまま網羅の穴になる。
- 禁止組合せを制約として書き出す:ある値が別の値を無効化する依存を、
if os=mac then node≠18 のような制約として列挙する。制約を入れないと無効ケースを量産し、build() がそもそも成立しないケースで赤になる。
- 強さ t を選ぶ:既定は 2-way(pairwise)。経験則上ほとんどの相互作用バグはここで出る。特に絡むと分かっている軸群だけ後で 3-way へ上げる(全体を 3-way にするとケースが急増する)。
- covering array をツールに生成させる:手で組合せを選ばない(必ず漏れる)。ライブラリ(
@fast-check/vitest の pairwise、allpairs、ACTS/PICT などの外部ツール出力を取り込む)で、制約を満たしつつ全 t-組合せを含む配列を生成させる。制約対応の生成器を使い、手順2の禁止組合せを入力する。
- 各行を1テストケースにする:生成された配列を
it.each(...) に渡し、各行で対象を実行してアサートする。行ごとに独立して落ちる形にし、どの組合せで落ちたかが出力で分かるようにする(%s 埋め込み)。
- t-way 網羅を検証する:生成器が本当に全 t-組合せを含んでいるか確かめる。小さな自前 assert(全パラメータ対の値の組合せが配列のどこかに現れるか走査)を1つ置くか、ツールのカバレッジレポートを確認する。「ツールを呼んだ」だけで網羅したと思い込まない。
完了チェック(もれ確認)
- 全パラメータ軸が列挙されたか:対象が分岐に使う入力で、手順1の表に載っていない軸が無いか。落とした軸はゼロケースで、網羅の対象外。
- 無効組合せが制約で除外されているか:生成ケースに、手順2の禁止組合せ(成立しない設定)が混じっていないか。混じれば偽陽性の赤を生む。
- 生成がツール由来か:covering array を手で選んでいないか(手選びは t-組合せを必ず取りこぼす)。
it.each に渡す配列がツール/関数の出力であることを確認する。
- t-way 充足が検証されたか:選んだ t に対し全 t-組合せが配列に含まれることを、自前 assert かツールレポートで確認したか。未検証なら「網羅した」が無根拠。
- 3-way へ上げた範囲が絞られているか:全体を不用意に 3-way にしてケース爆発させていないか。上げたのは相互作用バグが疑わしい軸群だけか。
網羅設計での効かせ方
コンビナトリアルの網羅対象は「t-way covering array が含むべき全 t-組合せ」であり、これを選んだ強さ t で完全に含むことが網羅基準になる。
全組合せ網羅は爆発するので、欠陥が少数パラメータの相互作用に集中するという経験則を使い、t を小さく(まず 2-way)取って現実的なケース数で相互作用を覆う。
パラメータと値域の列挙が網羅の母集合を決めるので、軸の洗い出しが網羅の中核になる(挙げ損ねた軸は covering array に入らず永遠に未テスト)。
制約は母集合から無効組合せを削るもので、網羅対象を「成立しうる組合せ」に限定する役割を持つ。
各軸の値域は連続値そのままではなく、同値分割で代表値へ離散化してから covering array に載せる。ここで blackbox-partition.md の同値分割・境界値と接続する(各軸内の網羅は同値分割、軸間の網羅はコンビナトリアル)。
配分の原則は、単一軸内の境界は境界値分析で厚く取り、軸をまたぐ相互作用はコンビナトリアルで t-way に覆うことだ。
ファジング系は別ファイルへ
不正・極端な入力を流し込んで頑健性(クラッシュ・例外・不変条件違反が起きないこと)を叩く手法(ファジング、カバレッジガイデッドファジング)は、generative-fuzzing.md にまとめた。
oracle を別の参照で代替する手法は別ファイルへ
正解を直接持たないまま、別の参照や関係で正しさを判定する手法は、過去出力基準(スナップショット、承認テスト、仕様化テスト)を oracle-past-output.md、別実装基準(差分テスト)を oracle-differential.md、関係/モデル基準(メタモルフィックテスト、モデルベーステスト、形式検証連携)を oracle-relational.md にまとめた。
AI と非決定的出力は別ファイルへ
テストを書く道具としての LLM(AI/LLM 支援テスト生成)と、製品に組み込まれた非決定的出力そのものの検証(階層化品質設計)は、ai-nondeterministic.md にまとめた。