Property-Based Testing for Python IaC with Hypothesis

Example-based tests check the cases you thought of; property-based tests generate the ones you did not. This guide, part of testing Python IaC under Python IaC fundamentals and strategy, uses Hypothesis to test the naming, tagging, and validation logic inside your infrastructure helpers.

Context

Most IaC bugs are not in the cloud calls — they are in the Python that computes names, CIDRs, and tags before the provider ever runs. That pure logic is ideal for property-based testing: assert invariants (a generated name is always DNS-safe, a subnet always fits its VPC) across thousands of inputs, complementing the mock-based unit tests you already run.

Property testing Property testing: Invariant holds with 4 facets. Invariant holds Generator random inputs Property always DNS-safe Shrink minimal case Seed reproducible
Hypothesis generates inputs, checks the invariant, and shrinks any failure to a minimal case.

Prerequisites

Prerequisites Prerequisites: layered from pytest down to Python. pytest hypothesis Python
Prerequisites: the building blocks this section assembles.
  • Python 3.9+ with pytest and hypothesis installed
  • A pure function under test — name/tag/CIDR logic with no cloud calls
  • A clearly stated invariant the function must always satisfy
# CLI: confirm the tools are available
pytest --version && python -c "import hypothesis; print(hypothesis.__version__)"

Implementation

Suppose a helper builds a resource name from a service and environment. The invariant: the result is lowercase, <= 63 characters, and contains only DNS-safe characters, for any inputs.

Test loop Test loop: generate input then run function then check invariant then shrink on fail generate input run function check invariant shrink on fail
Each round generates inputs, checks the property, and shrinks any failure to a minimal reproducer.
# test_naming.py — property test for a resource-name builder
# CLI: pytest test_naming.py
import re
from hypothesis import given, strategies as st
from naming import resource_name   # the pure function under test

safe = re.compile(r"^[a-z0-9-]{1,63}$")

@given(service=st.text(min_size=1, max_size=40),
       env=st.sampled_from(["dev", "staging", "prod"]))
def test_name_is_dns_safe(service: str, env: str) -> None:
    name = resource_name(service, env)
    # Invariant: whatever the input, the output is a valid DNS label.
    assert safe.match(name), f"unsafe name: {name!r}"

If resource_name ever emits an uppercase letter or an over-long string, Hypothesis reports the exact minimal input that broke it.

Verification

Run the suite; a green run means the property held across every generated case. Record the failing seed when one fails so it is reproducible in CI.

Verification Verification: Test → Program → Mock/Cloud. Test Program Mock/Cloud invoke declare resolve assert
Verification: the test drives the program and asserts on resolved values.
# CLI: run and, on failure, re-run the printed seed deterministically
pytest test_naming.py -q

Gotchas & Edge Cases

Gotchas & Edge Cases Gotchas & Edge Cases: Where it breaks with 4 facets. Where it breaks Credentials auth & region State lock & drift Types schema mismatch Ordering dependency graph
Gotchas & Edge Cases: the boundaries where things break and what to check.

Flaky properties hide real bugs. If a test only sometimes fails, the invariant is too weak or the function is non-deterministic; pin randomness and tighten the property.

Over-broad generators. Generating arbitrary Unicode may surface inputs your system will never see; constrain strategies to realistic inputs so failures are actionable.

Slow suites. Property tests run many cases; cap max_examples for fast feedback locally and raise it in nightly CI.

FAQ

Does this replace mock-based tests?

No — it complements them. Use property tests for pure logic and Pulumi mocks for resource wiring.

Can I property-test synthesized output?

You can assert invariants over CDKTF-synthesized JSON, but that overlaps snapshot testing; property tests shine on pure helpers.

How many examples are enough?

The default (100) catches most issues; raise it for critical invariants and lower it where feedback speed matters.