property-based-testing

verified

b7920c29-67fa-408c-b62a-505282539d0d

Use when example-based tests aren't enough — test invariants with randomly generated inputs, shrinking to minimal counterexamples. Use Hypothesis (Python) or fast-check (JS).

Metadata

Skill ID
b7920c29-67fa-408c-b62a-505282539d0d
Version
1
Owner
387274b7-2891-478b-81b8-e11d5adb9319
Tags
testingproperty-based-testinghypothesisinvariantsshrinking
Signature
verified
Integrity
OK
Content hash
c1faad4fa933bb5008d62bd1cc39da6d023c27ce4f57cee7b7e60e2af420daac
Created
2026-08-15T05:27:07Z

Skill file

Raw skill file (markdown source)
# Property-Based Testing

**Use when** example-based tests cover only a handful of cases and you want automated discovery of edge cases. Write properties (invariants that hold for all inputs), and let the framework generate counterexamples.

## What is a Property?

A property is a statement that must be true for ALL valid inputs:

```python
# Example-based (tests 3 specific inputs):
def test_reverse_examples():
    assert reverse([1,2,3]) == [3,2,1]
    assert reverse([]) == []
    assert reverse([5]) == [5]

# Property-based (tested for hundreds of random inputs):
from hypothesis import given, strategies as st

@given(st.lists(st.integers()))
def test_reverse_roundtrip(xs):
    """Reversing twice returns the original list."""
    assert reverse(reverse(xs)) == xs

@given(st.lists(st.integers()))
def test_reverse_preserves_length(xs):
    """Reversing doesn't change length."""
    assert len(reverse(xs)) == len(xs)
```

## Common Properties (Invariants)

| Domain | Property | Hypothesis test |
|--------|----------|-----------------|
| Encode/Decode | `decode(encode(x)) == x` | Round-trip identity |
| Sort | `is_sorted(sort(xs))` and `multiset(sort(xs)) == multiset(xs)` | Sort idempotence + element preservation |
| Serialize | `deserialize(serialize(obj)) == obj` | Round-trip |
| Arithmetic | `add(a, b) == add(b, a)` | Commutativity |
| Transform | `transform(a + b) == transform(a) + transform(b)` | Linearity |
| Merge | `merge(xs, []) == xs` | Identity element |

## Strategies for Generating Inputs

```python
from hypothesis import strategies as st

# Primitive strategies
@given(st.integers(min_value=0, max_value=10000))
def test_positive_integers(n): ...

@given(st.text(alphabet="abcdef", min_size=1, max_size=100))
def test_non_empty_strings(s): ...

@given(st.floats(allow_nan=False, allow_infinity=False))
def test_finite_floats(f): ...

# Composite strategies
@given(st.lists(st.integers(), min_size=0, max_size=100))
def test_lists(xs): ...

@given(st.dictionaries(st.text(), st.integers()))
def test_dicts(d): ...

@given(st.datetimes())
def test_dates(dt): ...

# Custom composite
@st.composite
def order_payloads(draw):
    return {
        "id": draw(st.uuids()),
        "items": draw(st.lists(st.text(), max_size=5)),
        "total": draw(st.floats(min_value=0, max_value=1000)),
    }

@given(order_payloads())
def test_process_order(payload):
    result = process(payload)
    assert result["status"] in ("ok", "error")
```

## Shrinking to Minimal Counterexamples

When a property fails, Hypothesis shrinks the failing input to the simplest reproduction:

```python
@given(st.lists(st.integers()))
def test_sort_idempotent(xs):
    assert sort(sort(xs)) == sort(xs)

# Hypothesis output:
# Falsifying example: test_sort_idempotent(xs=[0, 0])
# (Discovered [5, 3, 1, 5, 3, 1, ...] and shrank to [0, 0])
# This tells you: sort fails when there are duplicates!
```

## Combining with Example-Based Tests

Properties are powerful but cryptic. Use both:

```python
# Example-based: readable, specific, documents intent
def test_sort_empty():
    assert sort([]) == []

def test_sort_single():
    assert sort([42]) == [42]

def test_sort_already_sorted():
    assert sort([1, 2, 3]) == [1, 2, 3]

# Property-based: catches edge cases you didn't think of
@given(st.lists(st.integers()))
def test_sort_preserves_length(xs):
    assert len(sort(xs)) == len(xs)

@given(st.lists(st.integers()))
def test_sort_elements_preserved(xs):
    assert sorted(sort(xs)) == sorted(xs)
```

## Hypothesis Settings

```python
from hypothesis import given, settings, strategies as st

@given(st.lists(st.integers()))
@settings(max_examples=500, deadline=2000)  # 500 cases, 2s each max
def test_expensive_property(xs):
    ...

# Or configure globally in conftest.py:
# from hypothesis import settings
# settings.register_profile("ci", max_examples=1000, deadline=None)
# settings.load_profile("ci")
```

## Guardrails

- Properties must be **falsifiable but true**. "sort(xs) is a list" is too weak (vacuously true). "sort(xs) == xs" is too strong (false for unsorted input).
- Set a `deadline` — properties that run too slow will be flagged.
- Avoid non-deterministic properties (random, time, network). Properties must be deterministic.
- Don't generate unbounded inputs — constrain with `min_value`/`max_value`/`max_size`.

## Pitfalls

| Pitfall | Fix |
|---------|-----|
| Properties that are always true | Strengthen: add `and len(result) == len(input)` |
| Properties that are sometimes false | Check: is your generator producing invalid inputs? Filter with `assume()` |
| Slow generators | Constrain sizes; use `@settings(max_examples=50)` |
| Non-deterministic property (flaky) | Don't use `datetime.now()` or `random` inside the property |
| Confusing failure output | Read the "Falsifying example" — it's the minimal repro |

## Verify / Checklist

- [ ] At least one property per new function (round-trip, idempotence, length preservation)
- [ ] Example-based tests still exist for readability and documentation
- [ ] Strategies are constrained (no unbounded integers, no infinite strings)
- [ ] Hypothesis settings configured for CI (deadline, max_examples)
- [ ] `assume()` used to filter invalid inputs (not as a crutch for weak properties)
- [ ] Shrinking produces readable counterexamples

Attached files

No attached files.