Property-Based Testing skill for Codex

State the rule, let the library hunt the counterexample. Codex turns contracts into properties: round-trips for codecs, invariants for calculations, oracles for rewrites and model-based state machines for stateful components. It builds generators that match the domain, proves each property can fail, replays counterexamples by seed and pins every one it finds as a permanent example test.

  • Testing & QA
  • $property-based-testing
  • 199 lines
  • 2 files
  • ~2,000 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- property-based-testing

Installs to ~/.agents/skills/property-based-testing/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $property-based-testing, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Writes property-based tests with fast-check (TypeScript/JavaScript) and Hypothesis (Python) - finding properties such as round-trips, invariants, oracles and metamorphic relations, building generators and composite strategies, stateful model-based tests, shrinking to minimal counterexamples, and replaying failures by seed. Use when testing parsers, serializers, encoders, sorting or pricing logic, data structures and state machines, or when example-based tests keep missing edge cases.

Use it when

  • Testing parsers, serializers or encoders
  • Business rules have invariants that examples only sample
  • Replacing an implementation and needing to match the old one
  • Stateful components where call order matters

Use something else when

  • You need long coverage-guided campaigns against untrusted input: use fuzz-testing
  • Plain example cases already cover a small, closed input space: use unit-testing-patterns

How to check the result

Each property finds and shrinks a counterexample when the implementation is deliberately broken, and every counterexample found is pinned as an explicit example test.

The mistake it prevents

Writing a property that recomputes the expected value with the same algorithm as the code under test, so it can never disagree.

~/.agents/skills/property-based-testing/SKILL.md

--- name: property-based-testing description: Writes property-based tests with fast-check (TypeScript/JavaScript) and Hypothesis (Python) - finding properties such as round-trips, invariants,… license: MIT ---

A property test states a rule that must hold for every valid input, lets the library search for a counterexample, and when one is found reports the smallest input that breaks the rule, reproducible by seed. Done well, each property encodes a real requirement and generators produce the inputs production actually sees.

Inspect first

  1. Read AGENTS.md and check whether fast-check (and @fast-check/vitest or @fast-check/jest) or hypothesis are already dependencies. Adding them needs network access to install.
  2. Find the unit’s contract: types, validation rules, documented invariants. Properties come from the contract, not from the implementation.
  3. Look for existing example-based tests; keep them. Properties complement examples.

Finding properties

PatternPropertyTypical target
Round-tripdecode(encode(x)) == xserializers, parsers + printers, compression, URL encoding
Invariantoutput always satisfies a rulesorted output is ordered and a permutation of input; totals never negative
Oracle / differentialfast(x) == reference(x)optimized vs naive implementation, new vs legacy code path
Idempotencef(f(x)) == f(x)normalization, formatting, dedupe, upserts
Metamorphicchanging input in a known way changes output predictablyadding an item never lowers the cart total; filtering more never returns more
Commutativity / associativityorder of operations does not mattermerging CRDTs, set operations, money arithmetic
Never crashesarbitrary input yields a value or a typed errorparsers, request validators

Avoid properties that re-implement the function under test. If the only way to state the expected output is the same algorithm, use an oracle that is simpler (brute force) or a different pattern.

fast-check

import fc from "fast-check";
import { describe, it, expect } from "vitest";
import { encodeQuery, decodeQuery } from "./query";

describe("query string codec", () => {
  it("round-trips any flat string map", () => {
    fc.assert(
      fc.property(fc.dictionary(fc.string(), fc.string()), (params) => {
        expect(decodeQuery(encodeQuery(params))).toEqual(params);
      }),
    );
  });

  it("never makes the total smaller when an item is added", () => {
    const item = fc.record({
      priceCents: fc.integer({ min: 0, max: 1_000_000 }),
      qty: fc.integer({ min: 1, max: 99 }),
    });
    fc.assert(
      fc.property(fc.array(item, { maxLength: 30 }), item, (cart, extra) => {
        expect(total([...cart, extra])).toBeGreaterThanOrEqual(total(cart));
      }),
      { numRuns: 500 },
    );
  });
});
  • Async code: fc.asyncProperty and await fc.assert(...).
  • @fast-check/vitest (or @fast-check/jest) offers test.prop([fc.integer()])("...", (n) => ...) for tighter integration.
  • Useful arbitraries: fc.integer, fc.double({ noNaN: true }), fc.string, fc.uniqueArray, fc.record, fc.oneof, fc.constantFrom, fc.option, fc.date, fc.emailAddress, fc.uuid, fc.letrec for recursive structures.
  • Restrict with .filter() sparingly; heavy filtering wastes runs. Build valid values directly with .map() or fc.record instead.
  • On failure fast-check prints seed and path; replay exactly with fc.assert(prop, { seed: <seed>, path: "<path>", endOnFailure: true }). Then copy the shrunk counterexample into a plain example test so it runs forever.

Hypothesis

from hypothesis import given, example, settings, strategies as st

money = st.integers(min_value=0, max_value=10_000_000)
line = st.builds(LineItem, price_cents=money, qty=st.integers(min_value=1, max_value=99))

@given(st.lists(line, max_size=30), line)
def test_adding_an_item_never_lowers_total(cart, extra):
    assert total(cart + [extra]) >= total(cart)

@given(st.dictionaries(st.text(), st.text()))
@example({"": ""})                       # pinned edge case, always runs
def test_query_round_trip(params):
    assert decode_query(encode_query(params)) == params

Composite strategies for dependent values:

from datetime import date, timedelta

@st.composite
def date_ranges(draw):
    start = draw(st.dates(min_value=date(2000, 1, 1), max_value=date(2100, 1, 1)))
    days = draw(st.integers(min_value=0, max_value=365))
    return start, start + timedelta(days=days)

@given(date_ranges())
def test_nights_is_never_negative(rng):
    assert nights(*rng) >= 0
  • st.from_type(MyDataclass) and st.builds(...) infer from type hints.
  • assume(cond) discards an input; if many inputs are discarded, Hypothesis raises a health check error. Fix the strategy instead.
  • Hypothesis stores failing examples in .hypothesis/ and replays them first on the next run. Add .hypothesis/ to .gitignore; pin important ones with @example.
  • Profiles for speed vs depth:
# conftest.py
import os
from hypothesis import settings
settings.register_profile("ci", max_examples=500, deadline=None)
settings.register_profile("dev", max_examples=50)
settings.load_profile(os.getenv("HYPOTHESIS_PROFILE", "dev"))

deadline=None avoids false failures on slow CI machines; keep the default deadline locally to catch accidental slowness.

Stateful (model-based) testing

Compare the real system against a simple model across random command sequences.

from hypothesis.stateful import RuleBasedStateMachine, rule, invariant
import hypothesis.strategies as st

class CartMachine(RuleBasedStateMachine):
    def __init__(self):
        super().__init__()
        self.cart = Cart()
        self.model: dict[str, int] = {}

    @rule(sku=st.sampled_from(["A", "B", "C"]), qty=st.integers(1, 5))
    def add(self, sku, qty):
        self.cart.add(sku, qty)
        self.model[sku] = self.model.get(sku, 0) + qty

    @rule(sku=st.sampled_from(["A", "B", "C"]))
    def remove(self, sku):
        self.cart.remove(sku)
        self.model.pop(sku, None)

    @invariant()
    def matches_model(self):
        assert self.cart.quantities() == self.model

TestCart = CartMachine.TestCase

fast-check has the same idea with fc.commands([...]) and fc.modelRun(setup, cmds). Use stateful tests for caches, queues, undo stacks, connection pools and anything with an API where call order matters.

Generators that match reality

  • Bound sizes and ranges to the domain (quantities 1-99, prices in cents up to a cap). Unbounded integers find overflow bugs that cannot happen, which wastes time.
  • Do include the nasty edges the domain allows: empty strings, unicode, zero, maximum values, duplicate keys, leap days, DST transitions.
  • Floats: decide whether NaN and infinities are valid inputs; exclude them explicitly if not, and compare with tolerances.
  • Share generators in one module (tests/strategies.py, test/arbitraries.ts) so domain constraints stay consistent across properties.

Gotchas

  • A property that passes on the first run may be vacuous. Break the implementation on purpose and confirm the property finds it and shrinks to a small input.
  • Shrinking needs generators built from library combinators; values produced through external randomness (Math.random() inside the test) cannot shrink or replay.
  • Do not catch broad exceptions inside the property; a swallowed error passes.
  • Tests that hit a database or network per example are slow; lower numRuns / max_examples or test the pure core with properties and the edges with examples.
  • Properties do not replace coverage-guided fuzzing for parsers of untrusted input; for that, hand off to $fuzz-testing.

Verification

  1. Each property fails against a deliberately broken implementation and reports a shrunk counterexample.
  2. Every counterexample found during the work is pinned as an example test.
  3. Runtime per property stays under a few seconds in the default profile.
  4. CI runs with the deeper profile (more examples) at least nightly.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Property-Based Testing"
  short_description: "State the rule, let the library hunt the counterexample"
  default_prompt: "$property-based-testing Add property-based tests for our CSV parser and serializer round-trip"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem