Skip to content

Latest commit

 

History

History
158 lines (121 loc) · 7.46 KB

File metadata and controls

158 lines (121 loc) · 7.46 KB

hypothesis-recipes

Reusable, shrink-friendly Hypothesis strategies for the domains where hand-rolling st.* goes wrong.

You reach for property-based testing, and within ten minutes you're yak-shaving a strategy for "a valid timezone-aware datetime that isn't stuck in a DST gap", or "money as a Decimal with the right number of cents", or "a GeoJSON polygon whose ring is actually closed". Get it subtly wrong and you either miss bugs (your generator never produces the nasty input) or drown in false failures (your generator produces invalid input your code was right to reject). hypothesis-recipes is a library of those strategies, already debugged, already shrinking cleanly.

from hypothesis import given
from hypothesis_recipes import money_amounts, bounding_boxes, ipv4_addresses

@given(money_amounts(currency="JPY"))
def test_no_fractional_yen(amount):
    assert amount == amount.to_integral_value()   # JPY has 0 minor units

@given(bounding_boxes())
def test_box_is_well_formed(bbox):
    west, south, east, north = bbox
    assert west <= east and south <= north        # never inverted

@given(ipv4_addresses())
def test_parses(addr):
    import ipaddress; ipaddress.ip_address(addr)   # always valid

Sample draws from examples/demo.py:

money.money (amount, currency):
    ["2026.67", "GBP"]
    ["3.865", "KWD"]
    ["254748", "JPY"]

geo.geojson_points:
    {"type": "Point", "coordinates": [125.766..., -72.375...]}

net.urls:
    "http://sl9hti-ze0nirvc7x-b84x7u-f/iO9AArBz/b/Hgyck/1?xhc9=So&1e100=f"

Why this exists

The hard-won lesson of property-based testing is that the strategy is where the bugs hide — both the bugs in your code and the bugs in your test. Two failure modes dominate:

  1. Generating invalid data. A latitude of 95.0, a datetime at 02:30 on a spring-forward night, a bounding box with min > max. Your code correctly rejects it, Hypothesis reports a "failure", and you've learned nothing.
  2. Leaning on .filter(). The lazy fix for (1) is to generate junk and filter it out. That shrinks poorly, and Hypothesis will eventually raise FailedHealthCheck when it can't find enough valid examples.

Every recipe here is written constructively instead: a valid IP is built from an integer via ipaddress, a bounding box sorts its own corners, a business-day date is drawn as an index into the sequence of weekdays. Nothing is filtered, so everything shrinks to a small, readable counterexample. If you want the theory behind writing your own, the site has a walkthrough of building custom strategies that shrink well.

Running it

hypothesis-recipes is a small pure-Python package. Its only dependency is hypothesis itself. It is meant to be used from a clone — add the checkout to your PYTHONPATH and import the strategies.

git clone https://github.com/python-testing-debugging/hypothesis-recipes.git
cd hypothesis-recipes
python -m pip install hypothesis          # the one runtime dependency

# use it from your own project by pointing PYTHONPATH at the checkout:
PYTHONPATH=/path/to/hypothesis-recipes python -m pytest

Then, in your tests:

from hypothesis_recipes import dates, money, geo, json_api, text, net
# or grab specific strategies from the top level:
from hypothesis_recipes import aware_datetimes, slugs, json_values

The modules

Module What it generates
hypothesis_recipes.dates naive/aware datetimes, DST-safe datetimes, date ranges, business days
hypothesis_recipes.money Decimal amounts with exact minor units, currency codes, (amount, ccy)
hypothesis_recipes.geo latitudes/longitudes, bounding boxes, GeoJSON Point/Polygon
hypothesis_recipes.json_api recursive JSON, schema objects, ISO-8601 stamps, pagination, resources
hypothesis_recipes.text slugs, emails, usernames, identifiers, "nasty" unicode strings
hypothesis_recipes.net IPv4/IPv6 addresses, ports, hostnames, URLs

Every public strategy is a function returning a SearchStrategy, takes keyword-only knobs with sane defaults, is fully type-hinted, and carries a >>> example in its docstring.

How it works

The whole library is an exercise in one principle — construct, don't filter — applied per domain:

  1. Bounded integers → valid objects. IP addresses come from st.integers(0, 2**32-1).map(IPv4Address); business days from an integer index into the Mon–Fri sequence; money from an integer count of minor units scaled by 10**-places. Integers shrink toward zero, so counterexamples are minimal and readable (0.0.0.0, the earliest weekday, Decimal("0.00")).
  2. Sort/close/clamp in a @composite. Bounding boxes draw two lats and two lons and sort them; GeoJSON polygons sample points around a circle and repeat the first as the last to close the ring. The result is always well-formed.
  3. Convert through an instant for time. DST-safe datetimes generate a UTC instant and astimezone it into a real zone — a converted instant can never land in a spring-forward gap or be an ambiguous fall-back fold time.
  4. Compose the built-ins. Where a plain composition of st.* already shrinks well (JSON via st.recursive, schema objects via st.fixed_dictionaries), the recipe just wires it up with good defaults.

The test suite pins a deterministic Hypothesis profile (derandomize=True, no deadline) so runs are reproducible and CI stays green — see the notes on keeping Hypothesis suites fast.

Demo

Draw a handful of examples from every strategy in one command:

PYTHONPATH=. python examples/demo.py

Or run the living-documentation property tests, which use the recipes to check real invariants (money round-trips through cents, JSON round-trips through json.dumps, every URL parses with urllib.parse):

PYTHONPATH=. python -m pytest examples/test_examples.py

Background reading

These recipes are companions to the property-based testing material at python-testing-debugging.com. If you want to go from using strategies to designing your own, start here:

License

MIT