Property-Based Testing Patterns for Parsers, Serializers, and Validators

Property-based testing (PBT) is especially effective at revealing subtle failures in modules that transform or accept varied input. The patterns below give ready-to-use property ideas and pragmatic guidance for parsers, serializers, and input validators—three module classes where AI-generated code commonly implements the easy path but misses malformed or adversarial inputs.

1. Parsers — robust round-trip and error invariants

Properties to check:

Round-trip fidelity: for any syntactically valid AST or data object x, serialize(parse(serialize(x))) == serialize(x). This detects non-deterministic or lossy parsing/serialization pairs.

Idempotence of normalization: normalizing twice is the same as normalizing once: normalize(normalize(s)) == normalize(s). Useful where parsers also canonicalize input.

Reject-then-parse symmetry: if an input is rejected, then any strictly smaller (or simpler) input derived by a shrinking function should also be rejected unless documented otherwise—helps find inconsistent acceptance conditions.

Generator notes: produce valid and near-invalid inputs (missing delimiters, extra whitespace, truncated tokens, invalid escapes). Include randomly ordered fields, duplicate keys, extreme-length tokens, and Unicode edge cases (surrogates, combining marks).

2. Serializers — consistency, safety, and length/boundary properties

Properties to check:

Valid-output parseability: for any object x within domain, parse(serialize(x)) reconstructs an object equivalent to x up to documented lossiness (use a domain-specific equivalence).

Size and truncation behavior: serializing large inputs should either succeed deterministically or fail with a documented error; assert no silent truncation: if length(input) > N and serializer accepts it, round-trip must preserve distinguishing data.

Character-escaping invariants: serialized output must not contain unescaped control characters; build generators that inject control bytes and verify serializer encodes or rejects them consistently.

Generator notes: include nested/deep structures to trigger recursion limits, huge numeric values, special floats (NaN/Inf), and mixed encodings (binary blobs inside text fields).

3. Input validators — safety, canonicalization, and boundary rules

Properties to check:

Acceptance monotonicity: if a validator accepts input a and b is a stricter form of a (e.g., removing optional fields or normalizing case), validator should still accept b when that behavior is specified; otherwise assert documented differences.

Sanitize then validate: sanitize(input) should never produce a value that fails validate(sanitize(input)). This exposes unsafe sanitizers that introduce invalid constructs.

No silent elevation: validation must not convert rejected inputs into accepted ones by coercion without explicit policy—create properties that apply small coercions and assert acceptance only when policy allows it.

Generator notes: craft adversarial inputs—overlong strings, embedded nulls, mixed-encoding payloads, control sequences, and inputs that exploit typical heuristics (e.g., HTML-looking content for XSS filters).

Practical patterns for AI-generated implementations

1) Ground properties in the spec or examples: when possible, extract formal constraints (types, ranges, grammar productions) from docs and encode them as properties. If spec is absent, use conservative invariants (idempotence, round-trip, no silent truncation).

2) Combine PBT with example-based anchors: keep a few deterministic unit tests for critical cases (known regressions, security inputs) alongside broad PBT suites.

3) Use shrinking to prioritize fixes: when PBT finds failures, rely on the framework’s shrinking to produce minimal counterexamples; inspect those first to understand classifier weaknesses common in AI outputs.

4) Fuzz+property hybrid for hostile input: pair PBT generators with fuzzers that mutate serialized blobs to find parser crashes and undefined behavior.

5) CI integration: run targeted PBT jobs in CI with a reproducible RNG seed, and store failing seeds/artifacts to reproduce agent-found issues locally.

Example small property (pseudo-code)

Property: parse(serialize(x)) ≈ canonicalize(x)

Generators: arbitrary domain objects with optional missing fields, extra unknown fields, Unicode edge cases, and extreme sizes.

Assertion: equivalence(parse(serialize(x)), canonicalize(x)) where equivalence applies documented canonical rules; on failure, assert produced counterexample is reproducible with seed logged.

When to prefer PBT vs. example tests

Use PBT when the input space is large or structured (formats, protocols, relaxed grammars). Prefer example tests for tiny, well-specified behaviors or for documenting intended edge cases. In practice, use both: examples for intent, PBT to discover the unintended.

Applying these patterns will surface many of the brittle behaviors AI-written code tends to miss: unhandled encodings, boundary truncation, inconsistent acceptance rules, and parser/serializer mismatches. Start with a few high-value properties per module and progressively add generators that model realistic adversarial inputs.

Sources

s Español