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 schemaProperty-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.