コンテンツにスキップ
検索語を入力してください

    プロパティベーステスト

    例表だけのテストは「書いた通り動く」ことは示すが、入力空間全体の法則は示さない。proptest は不変条件・往復・遷移の拒否ルールを広い入力で叩くのに向く。

    フィクスチャの組み立ては テストデータ、状態機械の形は 状態遷移、型の前提は ドメインモデリングクレートガイド(proptest) を参照する。

    プロパティテストがコストに見合う場合

    Section titled “プロパティテストがコストに見合う場合”

    不変条件が多入力にわたって成り立ち、例表は不完全または保守が面倒なときproperty-based testを使う。

    向いている対象:

    • 値オブジェクトコンストラクタと検証ルール
    • parse/formatとDTO TryFrom の往復
    • state machine遷移法則と拒否ルール
    • 金額、単位、タイムスタンプ境界挙動
    • 冪等handlerとprojectionリプレイ
    • redactionと安全 Display/Debug 契約

    挙動が小さな閉じたケース集合、propertyが構造上自明、失敗が有用な最小例にshrinkしない場合は通常の単体テストを優先。

    ドメイン crate では proptest を優先

    Section titled “ドメイン crate では proptest を優先”

    shrinking、regressionファイル、 composable strategyが不変条件テストに合うため、サーバー側ドメインcrateのデフォルト推奨は proptest。プロジェクトがすでに標準化している場合のみ quickcheck

    proptest[dev-dependency] に追加。generatorは #[cfg(test)] モジュールまたは tests/support に置き、本番ドメインコードには入れない。

    [dev-dependencies]
    proptest = "1"

    public コンストラクタ経由で生成する

    Section titled “public コンストラクタ経由で生成する”

    generatorは本番パスが構築できる値を出す必要がある。strategyがraw structリテラルやprivateフィールド設定をすると、テストは通っても実呼び出しは失敗しうる。

    use proptest::prelude::*;
    fn valid_request_id() -> impl Strategy<Value = RequestId> {
    "[1-9][0-9]{0,15}".prop_map(|s| RequestId::new(s).expect("strategy produces valid ids"))
    }
    proptest! {
    #[test]
    fn request_id_rejects_empty(input in "\\PC*") {
    prop_assume!(input.trim().is_empty());
    prop_assert!(RequestId::new(input).is_err());
    }
    #[test]
    fn request_id_accepts_non_empty(input in "[1-9][0-9]{0,15}") {
    prop_assert!(RequestId::new(input).is_ok());
    }
    }

    無効入力が重要ならraw stringまたはDTOを生成し TryFrom/constructor拒否をassert — 無効データ周りにドメイン型を構築しない。

    テスト内で法則に名前を付け、1 propertyに1焦点。

    Property kindExample law
    Round tripTryFrom::<Dto>::try_from(x.clone())? 後 serialize が元形状と等しい
    Idempotence同一コマンド 2 回適用で追加効果なし
    Invariant preservation有効 Money + 有効 Money が負結果を出さない
    Rejection非法遷移が常に同じ error バリアント
    Projection replay順序通り event を畳むと snapshot + tail ロードと等しい
    proptest! {
    #[test]
    fn money_addition_is_commutative(a in money_strategy(), b in money_strategy()) {
    prop_assume!(a.currency() == b.currency());
    prop_assert_eq!(a.clone() + b.clone(), b + a);
    }
    }

    前提を満たさない入力については、空虚な成功をアサートせず、prop_assume! で棄却する。

    state machine を strategy としてモデル化する

    Section titled “state machine を strategy としてモデル化する”

    ライフサイクルルールでは到達可能stateだけ出すstrategyを組み、遷移結果をassertする。

    fn waiting_request() -> impl Strategy<Value = WaitingRequest> {
    (valid_request_id(), valid_passenger_id())
    .prop_map(|(id, passenger)| WaitingRequest::new(id, passenger))
    }
    proptest! {
    #[test]
    fn assign_driver_advances_state(
    waiting in waiting_request(),
    driver in valid_driver_id(),
    ) {
    let outcome = waiting.assign_driver(driver)?;
    prop_assert!(matches!(outcome.state, EnRouteRequest { .. }));
    }
    }

    非法遷移ではinvalidなsource stateおよびactionを生成し、特定errorバリアントをassert — is_err() だけにしない。

    縮小処理がコンストラクタを迂回する値を生成しないようにする。空文字、ゼロ金額、あり得ない列挙バリアントへ縮小された場合は、ストラテジを修正するか prop_assume! を追加する。

    自明でない入力のバグには proptest-regressions で再現可能失敗を保存:

    [dev-dependencies]
    proptest = "1"
    proptest-regressions = "0.2"
    proptest_regressions::proptest_regressions! {
    regressions = "path/to/regressions.txt"
    }
    proptest! {
    #![proptest_config(ProptestConfig::with_cases(256))]
    #[test]
    fn regression_example(input in strategy()) {
    // ...
    }
    }

    実バグ修正を表すregressionファイルはコミットする。

    非決定論/I/O 境界をデフォルトで property test しない

    Section titled “非決定論/I/O 境界をデフォルトで property test しない”

    property testは純粋ドメイン関数と、注入clockまたは固定フィクスチャの決定論adapter向け。

    デフォルトで避ける:

    • proptest! 内のlive DBまたはnetwork
    • シードclock strategyなしのwall-clock時刻
    • テスト対象としてのloggingやmetrics副作用

    生成payloadでDTO変換、redaction、errorマッピングをテスト。repositoryは制御不能I/Oではなくfakeまたはin-memory portで。

    プロパティテストの役割
    Value objectconstructor 受理/拒否、往復
    Domain transition法則、非法遷移エラー
    Use casefake port での idempotency(実 infra ではない)
    Boundary DTO不正/生成 payload が型付きエラーにマップ
    Projectionリプレイ順序と checkpoint idempotency

    読みやすいシナリオはexampleベース、型安全性約束はcompile-fail(テストデータ 参照)。

    property testはケース数を増やす。ドメインcrateでは通常デフォルトで足りる。デバッグ時のみローカルでcasesを上げる。

    • crateが小さく高速でない限りCIでは ProptestConfig::with_cases をデフォルト近くに保つ
    • 特に遅いpropertyは文書化し別CI jobで走らせる場合のみ #[ignore]
    • 再現性を犠牲にしない限りCIでshrinkingを無効化しない

    Cargo.tomlproptest または quickcheck があるとき、このガイドと不変条件のトピックガイド(modeling、state transitions、boundaries、persistence)を テストデータ と一緒に読み込む。

    レビューでは、publicコンストラクタを迂回するgenerator、法則を述べない is_ok() のみのアサーション、破棄すべき入力の曖昧な扱い、非法遷移の is_err() のみ確認、ライブI/Oへのproperty testを指摘する。

    ジェネレータは公開コンストラクタを使っているか — High

    Section titled “ジェネレータは公開コンストラクタを使っているか — High”

    newtry_newTryFrom ではなく、生リテラルやプライベートフィールドでドメイン構造体を組み立てる proptest / quickcheck 戦略を指摘する。

    プロパティ内で非決定的 I/O は避けているか — High

    Section titled “プロパティ内で非決定的 I/O は避けているか — High”

    注入フェイクや固定クロックなしに、ライブDB、ネットワーク、壁時計に当たる proptest! ブロックを指摘する。

    前提条件は prop_assume! で強制されているか — Medium

    Section titled “前提条件は prop_assume! で強制されているか — Medium”

    ドメイン外入力を成功と失敗のどちらとも曖昧に扱うのではなく、明示的に破棄すべきプロパティを指摘する。

    各プロパティは名前付き不変条件か — Medium

    Section titled “各プロパティは名前付き不変条件か — Medium”

    法則(往復、冪等性、拒否ルールなど)を述べず、is_ok() だけを検証する、または非構造化出力を比較するだけのプロパティテストを指摘する。

    非法遷移は特定エラーまでテストされているか — Medium

    Section titled “非法遷移は特定エラーまでテストされているか — Medium”

    状態遷移 も照合する。呼び出し元がエラーバリアントに依存するのに、非法遷移で is_err() だけを確認するプロパティテストを指摘する。

    縮小済みケースの回帰ファイルはコミットされているか — Low

    Section titled “縮小済みケースの回帰ファイルはコミットされているか — Low”

    プロパティが微妙なバグを見つけ、最小反例を黙って消えさせたくないときは proptest-regressions を提案する。