Property-Based Testing with Hypothesis: Clamp, Parse, Merge, Bank
Hypothesis generates inputs to verify properties like bounds adherence (clamp returns lo <= y <= hi), idempotence (normalize_whitespace twice unchanged), differential agreement (parsers match on int-like strings), metamorphic invariance (variance unchanged by constant shift), and state invariants (bank balance >=0, matches ledger replay).
Define and Test Core Functional Properties
Property-based testing with Hypothesis uses @given and strategies like st.integers(-50_000, 50_000) to generate thousands of inputs (max_examples=300) and check invariants automatically, shrinking failures to minimal counterexamples.
For clamp(x, lo, hi), test lo <= clamp(x, lo, hi) <= hi across bounds from st.tuples(st.integers(-10_000, 10_000), st.integers(-10_000, 10_000)).map to ensure lo <= hi. Also verify idempotence: clamp(clamp(x, lo, hi), lo, hi) == clamp(x, lo, hi).
normalize_whitespace(s) collapses whitespace to single spaces; test idempotence with @example(" a\t\tb \n c ") and assert normalize_whitespace(normalize_whitespace(s)) == normalize_whitespace(s), plus leading/trailing strip invariance.
merge_sorted(a, b) implements two-pointer merge; validate against reference sorted(a + b) using sorted_lists = st.lists(st.integers(-10_000, 10_000), min_size=0, max_size=200).map(sorted), and check is_sorted_non_decreasing(out) where all(outi <= outi+1).
These catch edge cases like empty lists or extremes that manual tests miss.
Validate Parsers and Stats via Differential and Metamorphic Testing
Differential testing compares independent implementations on shared inputs. safe_parse_int uses regex +-?\d+ and int(t); safe_parse_int_alt manually parses sign, digits (ord(ch)-48), rejecting non-digits or len>2000.
Test agreement on int_like_strings(): @st.composite draws left_ws/right_ws (space/tab/newline, 0-5 chars), sign '', '+', '-', digits (ASCII 48-57, 1-300 chars). With deadline=200ms, assert both return True and equal values.
Rejection: for s with re.fullmatch(+-?\d+, s.strip()) None, safe_parse_int returns False; else if digits >2000 post-sign, 'too_big'; else True, int.
Metamorphic testing checks output invariance under input transforms. variance(xs) computes sample variance: mu = sum/len, sum((x-mu)^2)/(n-1). Test v >=0; for n<2, ==0; shifting by k=7 preserves v (math.isclose, rel_tol=1e-12). Use phases=Phase.generate, Phase.shrink, lists(-1000..1000, 0-80 elems), target(variance(xs)).
Simulate Stateful Systems with Invariants and Rules
RuleBasedStateMachine models mutable state like Bank(balance=0, ledger=). deposit(amt>0): balance +=amt, ledger.append('dep',amt). withdraw(amt>0 and <=balance): balance -=amt, 'wd'. replay_balance recomputes from ledger.
BankMachine: @initialize checks balance==0==replay. @rule(amt=st.integers(1,10_000)) for deposit. @precondition(lambda: balance>0) @rule(amt=1..10_000) withdraw with assume(amt<=balance). @invariant balance>=0 and replay==balance.
Hypothesis runs sequences of 1-10k ops, violating preconditions or invariants exposes bugs like negative balance or ledger drift. Run via pytest -q; all pass confirms robustness.
Integrate into pipelines: pip install hypothesis pytest, settings suppress HealthCheck.too_slow for compute-heavy tests.