Skip to content

Lesson 5 of 9 · 18m

Contract and Property-Based Testing

Pin the interface when you can't test the internals — schema contracts, and property tests that find edge cases you didn't think of.

Two test techniques solve the gaps in the pyramid: contract tests pin interfaces between services or layers, and property tests pin invariants across inputs you didn't enumerate.

Contract testing

When service A calls service B, each has assumptions about the other's payload. A contract test runs B's contract against A's expectations — a consumer-driven contract (Pact) or an OpenAPI schema that both sides validate against. The win: a breaking change on either side fails in CI before either deploys.

# Producer side: the handler must satisfy the published schema
assert openapi_schema.validate(response.json(), "/users/{id}", "get")

# Consumer side: the client must tolerate the schema's allowed responses
contract_test(fixture_response)  # the fixture is generated from the schema

Property-based testing

Instead of asserting f(2, 3) == 5, assert a property that holds for all inputs — f(x, y) never returns negative, parse(serialized(x)) == x, sorted(xs) is a permutation of xs:

from hypothesis import given
from hypothesis import strategies as st

@given(st.lists(st.integers()))
def test_sort_is_idempotent(xs):
    assert sorted(sorted(xs)) == sorted(xs)

Hypothesis generates hundreds of random inputs — including edge cases you'd never write: empty lists, negative ints, unicode bombs, huge values. When it finds a failure, it shrinks to the minimal failing example — the smallest input that still breaks the invariant.

When each pays

  • Contract tests: microservices, third-party APIs, frontend↔backend — anywhere two codebases share a boundary.
  • Property tests: parsers, serializers, codecs, math functions — anywhere the spec is an invariant, not an output table.

Field notes

  • Property tests complement unit tests — write the obvious examples as unit tests, then write the property that covers the cases you didn't think of.
  • Shrinking is the feature that makes property testing usable — a "found with random inputs" failure isn't useful without a minimal reproducer.