This document provides implementation details for the property-based tests added to resolve issue #561.
Located in src/test_transitions.rs (lines 125-189):
fn arb_status() -> impl Strategy<Value = RemittanceStatus>Generates all 6 RemittanceStatus values uniformly.
fn arb_valid_transition() -> impl Strategy<Value = (RemittanceStatus, RemittanceStatus)>Generates 13 valid transition pairs:
- 7 edges in the state machine graph
- 6 idempotent transitions (same state)
fn arb_invalid_transition() -> impl Strategy<Value = (RemittanceStatus, RemittanceStatus)>Generates 20+ invalid transition pairs:
- 10 from terminal states (Completed, Cancelled)
- 10+ invalid forward/backward transitions
Located in src/test_transitions.rs (lines 191-370):
All wrapped in proptest! { } macro block.
prop_terminal_states_are_immutable(status in arb_status())Verifies: Terminal states (Completed, Cancelled) cannot transition to any other state. Coverage: All 6 states × all 6 targets = 36 combinations Shrinking: Minimal reproducer is a single terminal state
prop_valid_transitions_allowed((from, to) in arb_valid_transition())Verifies: All valid transitions are allowed by can_transition_to().
Coverage: 13 valid transitions
Shrinking: Minimal reproducer is a single valid transition
prop_invalid_transitions_rejected((from, to) in arb_invalid_transition())Verifies: All invalid transitions are rejected by can_transition_to().
Coverage: 20+ invalid transitions
Shrinking: Minimal reproducer is a single invalid transition
prop_idempotent_transitions_allowed(status in arb_status())Verifies: Same-state transitions are always allowed. Coverage: All 6 states Shrinking: Minimal reproducer is a single state
prop_terminal_states_block_further_transitions((from, to) in arb_valid_transition())Verifies: If a transition leads to a terminal state, that state cannot transition further. Coverage: All valid transitions that lead to terminal states Shrinking: Minimal reproducer is a single valid transition to a terminal state
prop_no_cycles_in_state_graph((from, to) in arb_valid_transition())Verifies: No cycles exist in the state machine (except self-loops). Coverage: All valid transitions Shrinking: Minimal reproducer is a single valid transition
prop_disputed_only_from_failed(status in arb_status())Verifies: Disputed state can only be reached from Failed state. Coverage: All 6 states Shrinking: Minimal reproducer is a single state
prop_pending_is_initial_only(status in arb_status())Verifies: Pending is the only initial state; no other state transitions to Pending. Coverage: All 6 states Shrinking: Minimal reproducer is a single state
prop_non_terminal_states_have_exits(status in arb_status())Verifies: Every non-terminal state has at least one valid outgoing transition. Coverage: All 6 states Shrinking: Minimal reproducer is a single non-terminal state
prop_transition_validation_is_deterministic((from, to) in arb_valid_transition())Verifies: Calling can_transition_to() multiple times returns the same result.
Coverage: All valid transitions
Shrinking: Minimal reproducer is a single valid transition
Located in src/test_transitions.rs (lines 372-385):
test_state_machine_graph_coverage()Explicitly verifies all 7 valid edges exist:
- Pending → Processing, Cancelled, Failed
- Processing → Completed, Cancelled, Failed
- Failed → Disputed
test_terminal_states_comprehensive()Verifies that Completed and Cancelled cannot transition to any other state.
- proptest generates 100 test cases (default)
- For each case, the strategy generates a random input
- The test assertion is executed
- If all pass, the property is verified
- If any fail, proptest shrinks to minimal reproducer
If prop_invalid_transitions_rejected fails with:
(RemittanceStatus::Completed, RemittanceStatus::Processing)
proptest shrinks to this minimal case and saves it to:
proptest/regressions/src_test_transitions_rs.txt
On subsequent runs, this case is replayed first to ensure the fix works.
Pending ──→ Processing ──→ Completed (terminal)
│ │
└───→ Failed ──→ Disputed
│ │
└───────────┴──→ Cancelled (terminal)
- Pending → Processing
- Pending → Cancelled
- Pending → Failed
- Processing → Completed
- Processing → Cancelled
- Processing → Failed
- Failed → Disputed
- Completed
- Cancelled
- Pending
- Processing
- Failed
- Disputed
| From | To | Valid | Test |
|---|---|---|---|
| Pending | Processing | ✅ | prop_valid_transitions_allowed |
| Pending | Cancelled | ✅ | prop_valid_transitions_allowed |
| Pending | Failed | ✅ | prop_valid_transitions_allowed |
| Pending | Completed | ❌ | prop_invalid_transitions_rejected |
| Pending | Disputed | ❌ | prop_invalid_transitions_rejected |
| Processing | Completed | ✅ | prop_valid_transitions_allowed |
| Processing | Cancelled | ✅ | prop_valid_transitions_allowed |
| Processing | Failed | ✅ | prop_valid_transitions_allowed |
| Processing | Pending | ❌ | prop_invalid_transitions_rejected |
| Processing | Processing | ✅ | prop_idempotent_transitions_allowed |
| Completed | * | ❌ | prop_terminal_states_are_immutable |
| Cancelled | * | ❌ | prop_terminal_states_are_immutable |
| Failed | Disputed | ✅ | prop_valid_transitions_allowed |
| Failed | Pending | ❌ | prop_invalid_transitions_rejected |
| Disputed | * | ❌ | prop_invalid_transitions_rejected |
- Property tests: ~100 cases × 10 properties = 1000 test cases
- Time per case: <1ms
- Total time: <1 second
- Minimal: Only state enum values in memory
- No heap allocations per test case
- Suitable for CI/CD
- Linear with number of properties
- Constant with number of states (6)
- Constant with number of transitions (13 valid + 20+ invalid)
- proptest already listed as dev-dependency (v1.4)
- No changes needed
- Tests run as part of
cargo test --lib - No additional configuration
- Failures block PR merges
- proptest saves failing cases to
proptest/regressions/ - Failing cases replayed on subsequent runs
- Ensures fixes don't regress
- Only essential code included
- No verbose or redundant logic
- Clear, focused test names
- Comprehensive error messages
- Inline comments for each strategy
- Doc comments for each property
- Separate guides for developers
- Clear explanation of invariants
- Easy to add new properties
- Easy to add new states
- Easy to add new transitions
- Clear separation of concerns
cargo test --lib test_transitions prop_ -- --nocapturecat proptest/regressions/src_test_transitions_rs.txtPROPTEST_REGRESSIONS=src/test_transitions.rs cargo test --lib test_transitions prop_my_testIf a property test fails, add a unit test for the specific case:
#[test]
fn test_specific_failing_case() {
let from = RemittanceStatus::Pending;
let to = RemittanceStatus::Completed;
assert!(!from.can_transition_to(&to));
}Generate arbitrary sequences of transitions and verify invariants hold:
prop_arbitrary_sequences(transitions in vec(arb_valid_transition(), 1..10))Verify state machine safety under concurrent access:
prop_concurrent_transitions(transitions in vec(arb_valid_transition(), 1..10))Add failing cases discovered in production:
#[test]
fn test_production_regression_case_1() { ... }Integrate with libFuzzer for continuous fuzzing:
cargo fuzz run fuzz_transitions- proptest docs: https://docs.rs/proptest/
- State machine:
src/transitions.rs - Types:
src/types.rs - Tests:
src/test_transitions.rs - Guides:
PROPERTY_BASED_TESTS.md,STATE_MACHINE_TESTING_GUIDE.md
Property-based tests provide comprehensive verification that the remittance state machine:
- Enforces all valid transitions
- Rejects all invalid transitions
- Maintains terminal state immutability
- Prevents cycles and stuck states
- Behaves deterministically
This significantly reduces the risk of undetected edge cases in state transitions.