Skip to content
Innovialabs

Property-Based Testing: Hypothesis and fast-check in Practice

Advanced3 min

In short

Instead of a few examples you describe a property that holds for any input, and Hypothesis or fast-check generate hundreds of cases and shrink any failure down to a minimal example.

Properties instead of examples

A regular test checks an example: "for 2 and 3 the sum is 5". A property-based test checks a statement that must hold for any input: "the result of sorting is ordered and contains the same elements".

The library generates many inputs itself — empty lists, negative numbers, Unicode, very long strings — and looks for one that breaks the property. This complements regular tests well: examples pin down known behaviour, properties search for what you did not think of.

  • Python — Hypothesis.
  • JavaScript and TypeScript — fast-check.

Generators and shrinking

A generator (a strategy in Hypothesis, an arbitrary in fast-check) describes what data to produce: st.integers(), st.lists(st.text()), fc.array(fc.integer()), fc.record({...}). Generators compose, so you can build an order or a user object.

Shrinking is the key convenience. When a failure is found on a huge random input, the library simplifies it step by step until it gets a minimal example that still fails. Instead of a list of 80 random numbers, you see [0, -1].

Which properties to check

PropertyExample
Round-tripdecode(encode(x)) == x, parse(format(x)) == x
Invariantafter sorting, the length is the same and items are ordered
Idempotencenormalize(normalize(x)) == normalize(x)
Oraclethe fast function matches a simple slow version
Does not crashthe function never throws on any valid input

Start with round-trip and "does not crash": they are the easiest to formulate.

Hypothesis: money that does not split evenly

A function splits an amount in cents into n parts:

from hypothesis import given, strategies as st

def split_amount(total, n):
    part = round(total / n)
    return [part] * n

@given(st.integers(min_value=0, max_value=10**9),
       st.integers(min_value=1, max_value=100))
def test_parts_sum_to_total(total, n):
    assert sum(split_amount(total, n)) == total

Examples like (100, 4) pass, but Hypothesis quickly finds a violation and shrinks it to something like total=1, n=2: the parts do not add up to the original amount. The fix is to distribute the remainder:

def split_amount(total, n):
    q, r = divmod(total, n)
    return [q + 1] * r + [q] * (n - r)

fast-check: hand-rolled serialization

A simple function turns an object into a key=value&... string:

import fc from "fast-check";

const stringify = (o: Record<string, string>) =>
  Object.entries(o).map(([k, v]) => `${k}=${v}`).join("&");

fc.assert(
  fc.property(fc.dictionary(fc.string(), fc.string()), (obj) => {
    expect(parse(stringify(obj))).toEqual(obj);
  })
);

fast-check finds a key or value containing & or =, and the round-trip breaks. A manual test with { name: "Ali" } would never have shown this. The fix is to escape values, for example with encodeURIComponent.

How to adopt it without pain

  1. Pick pure functions: parsers, formatters, calculations, data transformations.
  2. Write one property and constrain the generator to the real domain (ranges, lengths).
  3. Save each counterexample as a regular example test: that is your regression test.
  4. Record the seed when CI fails: both libraries tell you how to reproduce the run.
  5. Do not duplicate the implementation inside the property, or the test will repeat the same bug.

Common mistakes

  • Generators that are too broad, so filters discard most cases.
  • A property that is trivially true and checks nothing.
  • Slow tests: heavy database work inside every iteration.

FAQ

No, they complement them. Examples document specific behaviour and business rules; properties search for edge cases that are hard to invent by hand.

We can help with this

If you'd rather get the result than figure it all out yourself — this is what we do every day.

Discuss a project

Read next

Have an idea or a task? Tell us about it

We'll reply within one business day and propose a solution. The consultation is free.