| name | experience-exploratory |
| description | 仕様の構造から機械的に導くのではなく、乱数と即興で対象を揺さぶって欠陥を狙うブラックボックス技法群。 test-catalog の手法カタログの一部。ランダム/アドホックファジング(Random/Ad Hoc Fuzzing、固定seedで不変条件だけ見張る軽量ファジング)、 探索的テスト(Exploratory Testing、チャーターとセッションノートで設計と実行を同時進行)、 アドホックテスト(Ad Hoc Testing、事前設計も記録も持たない最も非形式的な確認) を検証したい、または割り当てたいときに使う。通常は test-catalog スキルの索引経由で 手法が選定された後にこのスキルを直接参照する。
|
| disable-model-invocation | true |
ブラックボックス設計技法: 乱数・探索ベース(乱数と即興で壊す)
仕様の構造から機械的に導くのではなく、テスト担当者の経験・直感・乱数から欠陥を狙う非形式的な技法のうち、乱数と即興を起点にするもの(ランダムファジング、探索的テスト、アドホックテスト)を扱う。
経験を確認項目リストに外部化して消し込むもの(チェックリストベーステスト、エラー推測)は experience-checklist.md を参照。
業務フロー(ユースケース、シナリオ)とフォーマット文法(構文テスト)を起点にするものは experience-scenario.md を参照。
カバレッジ計装で経路を掘る本格的な生成系ファジング(coverage-guided fuzzing)や、性質で出力の正しさまで縛る PBT は generative-property.md / generative-fuzzing.md を、oracle 代替はゴールデン/承認が oracle-past-output.md、差分が oracle-differential.md、メタモルフィック/モデルベース/形式検証連携が oracle-relational.md を参照(役割分担: ここは軽量・経験ベース、本格的な生成と oracle は向こう)。
これらは体系的技法の網を補う仕上げであり、単独で網羅性を主張しない。
見つけた欠陥は体系的技法へ書き戻して資産化(回帰テスト化)する。
目次
探索的テストとアドホックテストは本質的に非定型(設計と実行を同時進行する即興)なので、遂行手順を機械化しない。停止規範のみを定める。
ランダム / アドホックファジング(Random / Ad Hoc Fuzzing)
概要
ランダムに生成した入力を対象へ大量に投げ、クラッシュや不変条件違反だけを見張る軽量・経験ベースの壊し方。
エラー推測が「狙った1点」を当てるのに対し、こちらは「数で当てずっぽうに広く撒く」。
目的/いつ使う
入力空間が広く手で代表値を選びきれないとき、パーサやデコーダ、サニタイザを乱数入力で素早く揺さぶって「落ちないこと」を確かめたいとき。
ここで扱うのは手書きループ程度の軽量版に留める。カバレッジ計装で経路を掘り進める本格的な生成系ファジング(coverage-guided fuzzing)や、性質で出力の正しさまで縛る PBT は generative-property.md / generative-fuzzing.md を参照(役割分担: ここは軽量・経験ベース、本格的な生成と oracle は向こう)。
出力の意味的正しさを問いたいなら、ランダムファジングではなく PBT やメタモルフィック(oracle-relational.md)へ進む。ファジングが見られるのは原則「落ちないこと」だけ。
TypeScript example
固定 seed の擬似乱数で文字列を量産し、parse がクラッシュしないこと(=不変条件)だけを検査する。
import { describe, it, expect } from "vitest";
import { parse } from "./parser";
function mulberry32(seed: number) {
return () => {
seed |= 0;
seed = (seed + 0x6d2b79f5) | 0;
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
describe("parse: random fuzzing (crash-only)", () => {
it("never throws on random input", () => {
const rand = mulberry32(42);
for (let i = 0; i < 1000; i++) {
const len = Math.floor(rand() * 64);
const s = Array.from({ length: len }, () =>
String.fromCharCode(Math.floor(rand() * 0x110000)),
).join("");
expect(() => parse(s)).not.toThrow();
}
});
});
落とし穴
Math.random をそのまま使うと落ちた入力を再現できない。seed を固定し、失敗時はその入力をログに出して回帰へ昇格させる。
- 「落ちない」しか見ていないことを忘れ、正しさを検証した気になる。意味的正しさは PBT やメタモルフィックの領分。
- ランダムは浅い経路ばかり踏んで深い分岐に届かないことがある。経路を掘りたくなったら coverage-guided fuzzing(
generative-fuzzing.md)へ上げる合図。
遂行手順(着手→完了)
ファジングの本体は「何を不変条件として見張るか」を決め、落ちた入力を最小化して固定の回帰に変えることだ。乱数を撒いて緑を見て終わり、にしない。次の順で進める。
- 不変条件(oracle)を選ぶ:ランダム入力に対して常に成り立つべき性質を、強い順に検討して採れるものを採る。
- クラッシュしない:例外で落ちない(最弱・最汎用。パーサ/デコーダの最初の網)。
- 例外型が想定内:投げるなら宣言済みの型だけ(
SyntaxError は可、TypeError/RangeError の漏れは欠陥)。catch で型を判定する。
- round-trip 一致:
decode(encode(x)) === x や parse(format(x)) === x のように、可逆変換が往復で一致する(エンコーダ/シリアライザに最適)。
- 冪等:
f(f(x)) === f(x)(サニタイザ/正規化に最適)。
選べる不変条件が複数あるなら全部見張る。強い不変条件ほど欠陥を捕まえる。
- 再現可能な生成器を用意する:
Math.random を直接使わず、固定 seed の PRNG(上例の mulberry32 等)から入力を作る。seed をテスト内に明記し、失敗時に同じ列を再生できるようにする。
- 入力分布を対象に寄せる:純ランダムは構造を持つ入力(JSON, URL)の深い分岐に届きにくい。対象が構造化入力なら、有効入力に小さな変異(1文字置換/削除/挿入)を加える mutation 寄りの生成も混ぜる。
- 打ち切り条件を決めて回す:試行回数の上限(例 数百〜数千)、または時間上限を先に決める。CI で回すなら毎回 seed を変える運用にするか、固定 seed で決定的にするかを決める(flaky を避けるなら固定 seed、網を広げたいなら seed ローテーション + 失敗 seed の保存)。
- 失敗入力を最小化する:落ちた入力をそのまま回帰にすると巨大で読めない。**delta debugging(縮小)**で、落ち続ける範囲を保ったまま削る — 入力を半分に切って落ちる側を残す、を落ちなくなる直前まで繰り返す。1文字単位まで削れたらそれが最小再現。
- 最小入力を固定の回帰テストへ昇格する:最小化した入力を
it("regression: ...", ...) として通常テストに固定し、乱数ループとは別に残す(乱数は将来また別の入力を見つける網、固定回帰は既知欠陥の番人)。構文テスト・境界値へも書き戻す(experience-scenario.md の構文テスト)。
完了チェック(もれ確認)
- 不変条件が明示されているか:テストが見張る性質(クラッシュしない / 例外型 / round-trip / 冪等)がコード上で判別できるか。
not.toThrow しか無いなら、より強い不変条件(round-trip 等)を採れないか再検討したか。
- seed が固定で再現可能か:
Math.random を直接呼んでいないか(grep -n 'Math.random' <test> が0件)。seed がテストに明記され、同じ列を再生できるか。
- 失敗入力が最小化されたか:落ちた入力を生のまま回帰にしていないか。最小化(delta debugging)を経た短い入力になっているか。
- 固定回帰へ昇格したか:見つけた欠陥が乱数ループとは別の決定的テストとして残っているか(乱数ループだけだと次回 seed が変わると再現しない)。
- 打ち切り条件が決まっているか:試行回数/時間の上限が決まっているか。「とりあえず無限に回す」になっていないか(下の停止規範)。
- 意味的正しさを誤認していないか:「落ちない」を「正しい」と取り違えていないか。意味は PBT/メタモルフィック(
oracle-relational.md)の領分と切り分けているか。
「網羅」の扱い(網羅は主張しない)
- なぜ網羅を定義できないか:乱数で撒くため、踏んだ入力集合を形式的に覆えない(到達経路も保証できない)。
- 停止規範(どこまでやれば十分とするか):固定 seed での試行回数(例: 数百〜数千)か時間を上限に打ち切る。加えてコーパス飽和(新しいクラッシュ/不変条件違反が一定試行のあいだ1件も出なくなった、coverage-guided なら新規到達分岐が増えなくなった)を観測したら、その入力分布での実りは尽きたと見て打ち切る。落ちた入力は最小化して回帰テストへ固定する。
- 体系的技法への昇格:落ちた入力は構文テスト・境界値・回帰へ書き戻して資産化する。深い経路が要るなら coverage-guided fuzzing(
generative-fuzzing.md)へ移す。
探索的テスト(Exploratory Testing)
概要
テスト設計、実行、学習を同時に進め、直前の結果から次の操作を決めながら対象を調べる、構造化された即興。
目的/いつ使う
仕様が不完全あるいは流動的なとき、新機能の初見調査、自動テストでは気づけない使用感や整合の破れを探るとき。
時間を区切ったセッション(セッションベース管理)と憲章(チャーター)で散漫さを防ぐ。
回帰の自動化や厳密な合否判定には向かない。
コード例は不要、セッションノートで残す
即興ゆえテストコードに固定しない。
チャーターとセッションノートで記録する。
Charter: 決済フォームの入力検証を、貼り付け・IME・オートフィル経由で探る
Timebox: 60 min Tester: @you Build: 2026.6.1
Notes
- [bug] カード番号を全角で貼り付け→ 検証を素通り、API で 500
- [q] 有効期限の過去月、UI は通すが確定時のみ弾く。仕様か?
- [idea] オートフィルで姓名が逆。別チャーターで追う
Coverage: 検証ロジック 70% / 異常系の手応えは薄い
セッションで見つけた再現手順は、確定バグとして体系的技法(境界値、エラー推測)へ書き戻し、回帰テスト化する。
落とし穴
- 記録を残さず「触ってみた」で終わると再現も共有もできない。ノートが成果物。
- チャーター無しだと同じ所ばかり触る。範囲を宣言してから始める。
「網羅」の扱い(網羅は主張しない)
- なぜ網羅を定義できないか:設計と実行を同時進行する即興のため、テスト集合を全数で定義できない。
- 停止規範(どこまでやれば十分とするか):チャーター(範囲宣言)とタイムボックスとセッション数で打ち切り、セッションノートを成果物とする。
- 体系的技法への昇格:再現手順を体系的技法(境界値、エラー推測、回帰)へコード化して資産にする。
アドホックテスト(Ad Hoc Testing)
概要
事前設計も記録の枠組みも持たず、思いついたまま試す最も非形式的なテスト。
探索的テストからチャーターとノートの規律を外したもの。
目的/いつ使う
ごく短時間のスモーク確認、デモ前の最終チェック、不具合の素早い当たり付けなど、軽さが価値になる局面に限る。
品質保証の中心には絶対に置かない。
コード例は不要、その場の確認に留める
記録の型を持たないのが定義なので、コードもチェックリストも作らない。
価値が出たら(=同じ確認を繰り返したくなったら)それは即座に探索的テスト(ノート化)か体系的技法(コード化)へ格上げする合図。
アドホックのまま放置しない。
落とし穴
- 再現性も追跡性もゼロ。見つけた欠陥の条件をその場でメモしないと消える。
- 「テストした」と錯覚させる。網羅も合否基準も無いことを常に明示する。
「網羅」の扱い(網羅は主張しない)
- なぜ網羅を定義できないか:事前設計も記録も持たない最も非形式なテストで、対象の集合を定義しない。
- 停止規範(どこまでやれば十分とするか):スモークやデモ前の短時間確認に用途を限定し、「網羅も合否基準も無い」ことを常に明示する。
- 体系的技法への昇格:同じ確認を繰り返したくなったら即、探索的テスト(ノート化)か体系的技法(コード化)へ格上げする。
関連 reference