Skip to content

Latest commit

 

History

History
332 lines (269 loc) · 9.62 KB

File metadata and controls

332 lines (269 loc) · 9.62 KB

Property-Based Tests Implementation Notes

Overview

This document provides implementation details for the property-based tests added to resolve issue #561.

What Was Added

1. Test Strategies (3 functions)

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

2. Property-Based Tests (10 tests)

Located in src/test_transitions.rs (lines 191-370):

All wrapped in proptest! { } macro block.

Test 1: Terminal State Immutability

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

Test 2: Valid Transitions Allowed

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

Test 3: Invalid Transitions Rejected

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

Test 4: Idempotent Transitions

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

Test 5: Terminal Finality

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

Test 6: Acyclic Graph

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

Test 7: Dispute Reachability

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

Test 8: Initial State Uniqueness

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

Test 9: No Stuck States

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

Test 10: Deterministic Validation

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

3. Deterministic Tests (2 tests)

Located in src/test_transitions.rs (lines 372-385):

Test 1: State Machine Graph Coverage

test_state_machine_graph_coverage()

Explicitly verifies all 7 valid edges exist:

  • Pending → Processing, Cancelled, Failed
  • Processing → Completed, Cancelled, Failed
  • Failed → Disputed

Test 2: Terminal States Comprehensive

test_terminal_states_comprehensive()

Verifies that Completed and Cancelled cannot transition to any other state.

Test Execution Flow

Property Test Execution

  1. proptest generates 100 test cases (default)
  2. For each case, the strategy generates a random input
  3. The test assertion is executed
  4. If all pass, the property is verified
  5. If any fail, proptest shrinks to minimal reproducer

Shrinking Example

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.

State Machine Graph

Pending ──→ Processing ──→ Completed (terminal)
  │           │
  └───→ Failed ──→ Disputed
  │           │
  └───────────┴──→ Cancelled (terminal)

Valid Transitions (7 edges)

  1. Pending → Processing
  2. Pending → Cancelled
  3. Pending → Failed
  4. Processing → Completed
  5. Processing → Cancelled
  6. Processing → Failed
  7. Failed → Disputed

Terminal States (2)

  • Completed
  • Cancelled

Non-Terminal States (4)

  • Pending
  • Processing
  • Failed
  • Disputed

Test Coverage Matrix

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

Performance Characteristics

Test Execution Time

  • Property tests: ~100 cases × 10 properties = 1000 test cases
  • Time per case: <1ms
  • Total time: <1 second

Memory Usage

  • Minimal: Only state enum values in memory
  • No heap allocations per test case
  • Suitable for CI/CD

Scalability

  • Linear with number of properties
  • Constant with number of states (6)
  • Constant with number of transitions (13 valid + 20+ invalid)

Integration Points

Cargo.toml

  • proptest already listed as dev-dependency (v1.4)
  • No changes needed

CI/CD

  • Tests run as part of cargo test --lib
  • No additional configuration
  • Failures block PR merges

Regression Testing

  • proptest saves failing cases to proptest/regressions/
  • Failing cases replayed on subsequent runs
  • Ensures fixes don't regress

Code Quality

Minimal Implementation

  • Only essential code included
  • No verbose or redundant logic
  • Clear, focused test names
  • Comprehensive error messages

Documentation

  • Inline comments for each strategy
  • Doc comments for each property
  • Separate guides for developers
  • Clear explanation of invariants

Maintainability

  • Easy to add new properties
  • Easy to add new states
  • Easy to add new transitions
  • Clear separation of concerns

Debugging Failed Tests

Step 1: Identify Failing Property

cargo test --lib test_transitions prop_ -- --nocapture

Step 2: Check Regression File

cat proptest/regressions/src_test_transitions_rs.txt

Step 3: Replay Specific Case

PROPTEST_REGRESSIONS=src/test_transitions.rs cargo test --lib test_transitions prop_my_test

Step 4: Add Unit Test

If 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));
}

Future Enhancements

1. Sequence-Based Properties

Generate arbitrary sequences of transitions and verify invariants hold:

prop_arbitrary_sequences(transitions in vec(arb_valid_transition(), 1..10))

2. Concurrency Properties

Verify state machine safety under concurrent access:

prop_concurrent_transitions(transitions in vec(arb_valid_transition(), 1..10))

3. Regression Test Suite

Add failing cases discovered in production:

#[test]
fn test_production_regression_case_1() { ... }

4. Fuzzing Integration

Integrate with libFuzzer for continuous fuzzing:

cargo fuzz run fuzz_transitions

References

  • 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

Summary

Property-based tests provide comprehensive verification that the remittance state machine:

  1. Enforces all valid transitions
  2. Rejects all invalid transitions
  3. Maintains terminal state immutability
  4. Prevents cycles and stuck states
  5. Behaves deterministically

This significantly reduces the risk of undetected edge cases in state transitions.