テスト

プロパティベーステストとは?性質の書き方と縮小・再現の仕組みを実装者向けに解説

プロパティベーステスト(Property-Based Testing、PBT)は、入力と期待値の組を人が並べる代わりに、入力が満たすべき性質だけを宣言し、その性質を破る反例をフレームワークに探させるテスト手法です。実装者が最初につまずくのは書き方そのものではなく、どの処理に性質を見つけられるのか、そして反例が出たあと何を残すのかという運用の設計にあります。この記事では、例示ベーステストとの役割の違い、入力生成・縮小・シード再現というエンジンの内部動作、Hypothesis 6系やfast-check 4系など主要ライブラリの既定値、CIでの実行設定、採用を見送るべき条件までを整理します。

まとめ:往復変換と不変条件から始め、出た反例は回帰テストへ昇格させる

PBTは万能のテスト手法ではなく、性質を言語化できる処理にだけ刺さる道具です。導入の成否は、書き方の習得よりも対象選定でほぼ決まります。最初に手を付けるべきは、シリアライズとデシリアライズのような往復変換、ソートや集計のように結果側に不変条件が立つ処理、そして旧実装と新実装を突き合わせられる移行案件の3種類。

もう一つの分岐点が、反例が出たあとの扱いです。フレームワークは失敗した入力を最小形へ切り詰めて提示しますが、その入力を回帰テストとして固定しない限り、次回の実行では別の入力が生成されて同じ欠陥が見逃されます。反例の永続化までを工程に組み込んで、はじめてPBTは資産になる。

逆に、画面表示や自然文の生成のように「正しさ」を条件式で書けない領域へ持ち込むと、性質が空洞化して落ちないテストだけが残ります。後半では、採用を見送る条件と、導入が形骸化していく典型的な経路も具体的に示します。

プロパティベーステストの定義と例示ベーステストが取りこぼす欠陥

まず輪郭を固めます。PBTはテスト工程の名前ではなく、テストケースを誰が用意するかという分担の置き換えです。

入力が満たす性質を宣言してフレームワークに反例を探索させる定義

PBTでは、テストコードに具体的な入力値を書きません。書くのは「どんな入力に対しても成り立つはずの条件」だけです。たとえばJSONのシリアライザなら、任意のオブジェクトを文字列化してから読み戻した結果が元と等しいこと。この1文が性質(プロパティ)にあたり、実際の入力はジェネレータが生成します。

手法としての起点は、Koen ClaessenとJohn Hughesが2000年のICFPで発表したHaskell向けツールQuickCheckです。以降、各言語へ同じ設計思想の実装が移植され、現在の主要ライブラリはいずれもこの系譜に連なります。名前は言語ごとに違っても、性質の宣言・入力の自動生成・反例の縮小という3点セットは共通しています。

例示ベーステストが取りこぼす境界値と入力の組み合わせ爆発への対処

従来の書き方、つまり入力と期待値を1組ずつ並べる例示ベーステストが弱いのは、人が思いつく入力しか流れない点にあります。空文字、負のゼロ、サロゲートペア、桁あふれ寸前の整数。これらはレビューでも指摘されにくく、テストコードの行数を増やしても網羅されません。

引数が3つあり、それぞれに5通りの代表値を置いた時点で組み合わせは125通りになります。人手で書き切れる規模ではなく、実際には代表的な数通りだけが選ばれる。PBTはこの選択を機械に委ね、既定でも1回のテスト実行あたり数百件の入力を通します。テストカバレッジの計測で行や分岐の到達率を見ても、同じ行を通る入力値の多様性までは見えません。PBTが埋めるのはこの隙間です。

ファジングとの違いはクラッシュ以外の性質を検証するかという判定軸

ランダムな入力を大量に流す点で、PBTはファジングによる脆弱性の自動検出と混同されがちです。分ける軸は、何を失敗とみなすかにあります。

ファジングが検知するのは主にクラッシュ・ハング・メモリ破壊といった、プログラムが自己申告する異常です。入力は仕様を意図的に壊す方向へ変異させ、正しい出力が何かは問いません。対してPBTは、正常に返ってきた結果が性質を満たすかどうかまで検証します。処理が落ちなくても、往復変換で値が欠けていれば失敗と判定される。セキュリティ観点の堅牢性を見るならファジング、ロジックの正しさを見るならPBTという住み分けになります。

入力生成・縮小・シード再現で構成されるPBT実行エンジンの内部動作

PBTのライブラリは、生成・縮小・再現という3つの機構でできています。書き方の暗記より、この内部動作を理解しておくほうが実務では効きます。

ジェネレータが入力を生成する範囲と既定の試行回数100件の意味

ジェネレータは型ごとに用意された入力の供給源で、整数なら0や1、最大値・最小値といった境界付近を意図的に厚く出します。完全な一様乱数ではなく、欠陥が出やすい領域へ偏らせた分布になっている点が特徴です。

1つの性質に対して何件流すかは既定値が決まっており、Hypothesisのmax_examplesとfast-checkのnumRunsはいずれも100件、jqwikの@Propertyは1000回、Rustのproptestは256件です(各公式ドキュメント・2026-08-07時点)。100件という数字は網羅の保証ではなく、CIの実行時間と検出力の折り合いとして置かれた初期値にすぎません。重い処理では下げ、軽い純関数では桁を上げる。この調整は導入直後に必ず発生します。

縮小機能が反例を最小の入力まで切り詰める過程と失敗ログの読み方

PBTの実用性を支えているのは、生成よりむしろ縮小(shrinking)のほうです。ランダム生成された反例は、たいてい人間には読めません。要素が200個ある配列や、32桁の浮動小数点数が出てきます。

縮小は、失敗した入力から要素を削り、数値を0へ近づけ、それでも失敗し続ける最小の形を探し直す工程です。結果として報告されるのは「長さ2の配列」「値は1と-1」といった、原因を直接示す入力になります。ログを読むときは、生成された元の入力ではなく縮小後の値だけを見る。縮小後がごく単純な値なら実装の分岐漏れ、複雑なまま止まるなら性質の書き方自体が誤っている可能性を疑います。

seedとpathで反例を再現し回帰テストへ固定するまでの手順

ランダム生成である以上、再現できなければ調査に入れません。各ライブラリはこの前提に対応する仕組みを持っています。

  1. 失敗時に出力される乱数シード(fast-checkではseedpath)を控える
  2. 同じシードを指定して再実行し、同一の反例が出ることを確認する
  3. 縮小後の入力を例示テストとして固定し、通常のテストへ移す
  4. 修正後、シード指定を外した通常実行へ戻す

Hypothesisはこの工程の一部を自動化しており、失敗した入力をカレントディレクトリ配下の.hypothesis内にある例データベースへ保存し、次回実行時に先頭で再試行します。proptestも同様にproptest-regressionsファイルへ書き出す設計です。ただし、これらはローカルの成果物であり、CIコンテナでは実行のたびに消える。永続化を期待するならバージョン管理へ載せる判断が要ります。

Hypothesis・fast-check・jqwikの言語別ライブラリ選定基準

言語を選べる場面はまれで、実際には既存プロジェクトの言語で決まります。それでも既定値と依存条件は導入前に把握しておく価値があります。

PythonのHypothesisが備える例データベースと再実行の仕組み

PythonではHypothesisが事実上の標準で、2026-08-07時点の最新は6.165.2、動作にはPython 3.10以上が要ります。テスト関数に@givenを付け、引数の生成規則を渡す書き方で、pytestとそのまま併用できます。

他言語の実装と比べたときの強みは、前述の例データベースです。既定では.hypothesis配下のexamplesディレクトリに保存され、権限不足で書けない場合はメモリ上へ退避して警告を出します。個別の反例を恒久的に残したいときは@exampleデコレータで明示的に固定する。データベース任せにしないこの一手間が、チーム開発では効いてきます。

TypeScriptのfast-checkをJestやVitestへ組み込む構成

TypeScript・JavaScript側の選択肢はfast-checkで、2026-08-07時点の最新は4.9.0、Node.js 12.17以上で動きます。単体でテストランナーの役割は持たず、既存のJestやVitestのitの中でfc.assert(fc.property(...))を呼ぶ構成です。

既存のテスト資産を置き換えずに1ファイル単位で足せるため、導入コストはこの中で最も低い部類に入ります。試行回数はテストごとに第2引数で渡せるほか、fc.configureGlobalで全体の既定値を上書きできる。CIとローカルで値を変える運用は、この全体設定を環境変数で分岐させる形が扱いやすい構成です。

JavaのjqwikとRustのproptestで異なる既定試行回数の設計

Javaではjqwikが代表格で、2026-08-07時点の最新は1.9.3、JUnit Platform 1.14.4以上を要求します。JUnit 5の代替テストエンジンとして動くため、JUnitによる単体テストの実行基盤をそのまま使えます。既定の試行回数が1000回と他より1桁多いのは、JVM上の実行速度を前提にした設計判断です。

Rustのproptestは1.11.0(2026年3月公開、Rust 1.85以上)で既定256件。Goについては標準ライブラリにtesting/quickがありますが、公式ドキュメントに「frozen and is not accepting new features」と明記されており、新規採用の対象からは外れます。Go 1.18以降のネイティブなファジング機構か、サードパーティ製ライブラリを選ぶことになります。

主要4ライブラリの既定試行回数と動作要件を並べた選定用の比較表

ライブラリ 言語 既定の試行回数 動作要件 反例の永続化
Hypothesis 6系 Python 100件 Python 3.10以上 例データベース
fast-check 4系 TypeScript 100件 Node.js 12.17以上 手動で固定
jqwik 1.9系 Java 1000回 JUnit Platform 1.14.4以上 既定でファイル保存
proptest 1.11系 Rust 256件 Rust 1.85以上 回帰ファイル

版番号はいずれも2026-08-07時点の公式レジストリ実測値で、以降のマイナー更新で変わります。選定時に見るべきは最新版そのものではなく、既定試行回数と反例の永続化方式の差のほうです。

受託開発でプロパティを見つける手順と既存テストへの組み込み順序

導入相談で最も多い詰まりどころが「性質が思いつかない」という段階です。ここは発想力の問題ではなく、着眼点の型を知っているかどうかで決まります。

往復変換・不変条件・参照実装から性質を洗い出す実務の着眼点4つ

性質の見つけ方には定石があります。実務で使う頻度が高い順に4つ。

  • 往復変換:符号化して復号すると元に戻る。JSON・CSV・独自バイナリの入出力で真っ先に効く
  • 不変条件:ソート後も要素数と多重集合が変わらない、合計金額が明細の総和と一致する
  • 参照実装との一致:素朴で遅い実装と、高速化した本番実装の出力が常に等しい
  • 冪等性:同じ操作を2回適用しても結果が変わらない。再送やリトライを持つ処理で使う

この4つのうち、受託開発で最も出番があるのは1番目と3番目です。往復変換は外部連携のある案件ならほぼ必ず存在し、参照実装との一致はレガシー刷新で旧システムを参照側に置ける。逆に4番目まで持ち出す必要がある案件は多くありません。まず往復変換の1本を通し、動く実例を作ってから広げる順序が現実的です。

入力の前提条件を絞り込むフィルタと専用生成器を使い分ける判断基準

現実の関数は任意の入力を受け付けません。日付なら妥当な範囲、IDなら特定の形式という前提があります。これを表現する方法は2つあり、選び方を間違えるとテストが遅くなります。

1つはフィルタで、生成された入力のうち条件を満たさないものを捨てる方式。書くのは簡単ですが、棄却率が上がるほど無駄な生成が増え、ライブラリによっては生成失敗として打ち切られます。もう1つは専用の生成器を組み立てる方式で、最初から条件を満たす値だけを作ります。目安として、棄却率が1割を超えるなら生成器を書く。日付や識別子のように構造が決まっている入力は、最初から生成器側で表現したほうが速く、意図も読み取れます。

API連携のシリアライズ処理から導入する場合の対象選定と実施順序

既存プロジェクトへ後から入れるなら、着手点は外部連携の境界に置くのが定石です。リクエストとレスポンスの変換層は往復変換の性質がそのまま立ち、副作用を持たず、失敗したときの影響範囲も読みやすい。

順序としては、変換層で1本通す、次に金額計算や日付計算のような純粋なドメインロジックへ広げる、そのあとで状態を持つ処理を検討する、という3段構えです。データベースや外部APIを呼ぶ処理へ最初から当てると、実行時間と不安定さの両方を抱え込みます。API開発・システム連携のように外部システムとの入出力が多い案件では、この変換層のテスト強度が納品後の障害件数へ直結するため、投資対効果も説明しやすい領域です。

プロパティベーステストを採用しない3条件と典型的な失敗の導入パターン

ここは言い切ります。次の3条件のいずれかに当てはまる対象へPBTを持ち込むのは過剰であり、導入しないほうが総合的な品質は上がります。

性質を言語化できない画面表示やUI操作へ持ち込まないという判断

1つ目の見送り条件は、正しさを条件式で書けない対象です。画面のレイアウト、文言の自然さ、生成AIの出力品質。これらは「落ちないこと」しか書けず、性質としては空になります。

この領域はE2Eテストと結合テストの使い分けや人の目によるレビューが担う範囲で、入力を機械生成しても検出力は上がりません。無理に導入すると、何も検証していないテストがCI時間だけ消費する状態になります。

反例が出るたび前提条件を緩める運用が招く性質の形骸化という経路

2つ目は、仕様が固まっていない開発初期です。落ちるたびに「この入力は対象外」と前提条件を足していくと、最終的に元のテストで使っていた数件しか通らない性質が残ります。形は残るが中身は空という、最も典型的な失敗の経路です。

判別の目安は、前提条件を追加した回数。3回を超えたら性質の立て方を疑い、条件を足すのではなく性質そのものを書き直します。仕様が動いている最中なら、いったんテスト駆動開発(TDD)の基本サイクルで例示ベースの回帰網を張り、仕様が落ち着いてからPBTへ移すほうが手戻りは少なく済みます。

例示テストを置き換えず併存させる配分とレビュー体制側の前提条件

3つ目は体制側の条件です。PBTの失敗は「入力Xで落ちた」ではなく「性質Yが破れた」という形で報告されるため、読むには性質の意図を理解している必要があります。書いた本人しか読めないテストは、その人が抜けた時点で無効化されるだけです。

前提として、性質はレビュー対象に含める。そして例示テストは置き換えない。仕様書に書かれた代表ケースは例示テストとして残し、PBTはその周辺の入力空間を埋める役割に限定します。既存の単体テストを削ってPBTへ寄せる進め方は、テストの意図が読み取れなくなるため採りません。実務での配分は、既存の層構成を保ったまま変換層とドメインロジックにPBTを数本足す程度で十分に効きます。

CI上のフレーキー回避と実行時間を抑えるPBT運用設定の具体的な実務

導入後に問題化するのは、実行時間の増加とテストの不安定さです。どちらも設定で抑えられます。

試行回数と実行時間の上限をCIとローカルで切り替える設定の考え方

既定の100件を全テストに一律で適用すると、テスト数が増えたときにCIが伸びます。現実的な運用は、環境ごとに値を変える形です。

プルリクエスト時のCIは既定値のまま、あるいは半分程度まで下げて数分以内に収める。夜間バッチや週次のジョブでは10倍以上へ引き上げ、時間をかけて入力空間を掘る。ローカルでは開発者が待てる範囲、おおむね数十件で回します。fast-checkならfc.configureGlobal、Hypothesisならプロファイル設定で、この切り替えを1箇所に集約できます。

乱数シードを固定するか毎回変えるかで分かれる2つの運用方針の比較

シードを固定すれば実行結果は毎回同じになり、CIは安定します。ただし固定した瞬間、PBTは大量の例示テストと変わらなくなり、新しい入力を見つける力を失います。

推奨する方針は、CIではシードを固定せず、失敗時のシードをログへ必ず残す形です。ランダム性を残したまま調査可能性を確保できます。ただし例外があり、テストの失敗が即座にデプロイ停止へつながるリリース直前のパイプラインでは固定側へ倒す。判断軸は「失敗したとき、その場で調査できる体制があるか」の一点です。調査に入れない時間帯のジョブでランダム性を残すと、翌朝には再現しない失敗ログだけが残ります。

失敗した入力を回帰テストへ昇格させるレビュー時の確認項目と手順

反例が見つかったときの処理を定型化しておきます。確認するのは3点、縮小後の入力・修正コミット・固定した例示テストの3つがそろっているかです。

縮小後の入力をそのまま例示テストとして追加し、修正前のコードで確実に失敗することを確かめてから修正へ進みます。この順序を守らないと、直したつもりの箇所が実は無関係だったという取り違えが起こる。追加した例示テストは通常のリグレッションテストの範囲へ組み込み、以降は毎回実行されるようにします。PBT側の性質は残したまま、確定した反例だけを別レイヤーへ移す。この二重化で、同じ欠陥が再発したときの検出が確実になります。

よくある質問

導入検討の場で実際に挙がる質問を5つ挙げ、実装者の視点で回答します。

プロパティベーステストは単体テストの置き換えになりますか?

置き換えにはなりません。PBTは単体テストの書き方の一種で、既存の例示ベーステストと同じ層に並びます。仕様書に明記された代表ケースは例示テストとして残す必要があり、これを削るとテストコードから仕様が読み取れなくなります。実務での関係は補完で、例示テストが仕様の記述を担い、PBTがその周辺の入力空間を埋める分担です。

試行回数はどれくらいに設定すればよいですか?

Hypothesisとfast-checkの既定は100件、jqwikは1000回、proptestは256件です(2026-08-07時点)。まずは既定のままCIへ載せ、実行時間を計測してから調整します。目安として、1つの性質が1秒を超えるなら回数を下げるか対象処理を見直す。逆に純粋な計算処理でミリ秒単位なら、1000件以上へ上げても総実行時間への影響は小さく済みます。

実行のたびに結果が変わるとCIが不安定になりませんか?

入力が毎回変わるため、これまで通っていたテストが突然落ちることは起こります。ただしその多くは既存の欠陥を新しい入力が引き当てた結果で、テスト側の不具合ではありません。運用上は、失敗時のシードをログへ残し、その入力を例示テストへ固定してから修正する手順を決めておきます。再現できない失敗を放置しない仕組みさえあれば、不安定さは調査可能な形に収まります。

どの言語から始めるのが現実的ですか?

既存プロジェクトの言語をそのまま使うのが原則です。新規に試すならPythonのHypothesisかTypeScriptのfast-checkが扱いやすく、どちらもpytestやJest・Vitestへ1ファイル単位で足せます。学習用に別言語を選ぶより、実際の変換処理へ1本書いて反例が出る体験を先に作るほうが定着します。

ファジングやモックとはどう使い分けますか?

ファジングはクラッシュや脆弱性の検出が目的で、出力の正しさは問いません。PBTは正常応答の中身まで検証します。モックは外部依存を切り離す道具で、PBTと排他ではなく併用する関係です。ただしモックを多用した状態でPBTを書くと、検証しているのがモックの振る舞いだけという状態に陥りやすいため、副作用のない純粋な処理から適用するほうが確実です。

関連記事

資料請求

RELATED POSTS 関連記事