Appendix C - 12. FsCheck property tests

The Structure of FsCheck property tests — its shape at a glance:

A generator synthesizes inputs; the property runs on each and asserts the invariant. On a failure the framework shrinks the input to the smallest case that still breaks the invariant.

flowchart LR
  Gen[Input generator] -->|synthesized cases| Prop{Property holds?}
  Prop -->|yes| Pass([pass])
  Prop -->|no| Shrink[Shrink to minimal case]
  Shrink --> CE([counterexample])

Accessible description: a generator synthesizes inputs and a property checks the invariant on each. When the property holds the test passes; when it fails the framework shrinks the input to the smallest case that still breaks it and reports that counterexample.

Projected from the catalogue entry regression-tests / FsCheck property tests.

On this page: Intent · Motivation · Applicability · Structure · Sample Code · Consequences · Example use within DocAble · Related Patterns

Intent

Intent — Property-based tests that assert invariants (round-trip, combinatorial) over machine-generated inputs, catching bugs in the input space that example-based tests never reach.

Motivation

Example-based tests only check the cases you thought of. Invariants (read∘write = identity, a combinatorial property that must hold for all inputs) fail on inputs you never imagined and never wrote a test for. The failure is a bug living in the untested input space, and it recurs for any invariant-shaped contract that hand-picked examples under-cover.

Applicability

Structure

The Structure diagram appears at the top of this page.

Sample Code

A property test states a law and lets the framework hunt for a violation. The round-trip law — parse then serialize returns the original model — is the canonical shape for a typed document model. The generator supplies structured inputs; the framework shrinks a failure to the smallest model that still breaks the law, so the counterexample is debuggable.

from hypothesis import given, strategies as st

# a generator for the domain type — here a small structured document model
docs = st.builds(dict,
                 title=st.text(max_size=20),
                 nodes=st.lists(st.text(max_size=10), max_size=8))

@given(docs)
def test_parse_serialize_round_trips(doc):
    """Round-trip invariant: serialize then parse yields the same model, for
    *every* generated document — not just the examples someone thought to write.
    A failure shrinks to the smallest document that still breaks identity."""
    assert parse(serialize(doc)) == doc     # `parse`/`serialize` are the model's read/write

Consequences

Example use within DocAble

Contents
© James C. Davis, 2026–present