Skip to content

Round-Trip Property Test

Automated verification — instantiates Round-Trip Code Alignment

Generates a wide space of source values and asserts that decoding their encoding preserves the required invariants, so an encoder and its decoder can never silently drift apart.

The cheapest way for a codec to lie is for its two halves to disagree just off the beaten path — the encoder writes a shape the decoder reads back slightly wrong, and only a rare input reveals it. Round-Trip Property Test guards against exactly that by asserting a relationship rather than a value: for any generated source x, decode(encode(x)) must recover x up to the invariants that matter. It never needs to know the "correct" encoded bytes — it only needs the encoder and decoder to agree with each other across a huge, machine-generated sample of inputs, including the ugly edges a human would never think to type. That is what makes it THIS mechanism and not a fixed example set: it checks the round trip against itself, generatively, so drift shows up as a failing property instead of a production incident.

Example

A team's persistence layer serializes rich domain objects to a storage format and reads them back — an object-to-record-to-object round trip of the kind every ORM performs. Rather than hand-write cases, they state one property: for any object the generator can build, decoding its encoding equals the original. The generator manufactures thousands of objects — empty collections, deeply nested structures, Unicode names, extreme decimals, nulls in optional fields. The suite quickly finds a failure and shrinks it to a minimal reproducer: a timezone-naive timestamp comes back shifted by an hour. Two fields, not two thousand — the smallest input that still breaks. They fix the encoder, re-run, and green. From then on the property runs in CI, so any future edit that pushes encoder and decoder out of alignment fails the build the moment it lands.

How it works

  • Assert a relationship, not an answer. The oracle is decode(encode(x)) ≍ x under the fidelity invariants; no expected output is written by hand, so the test scales to inputs nobody enumerated.
  • Generate, then shrink. A generator samples the input space with a bias toward boundary and adversarial values; on failure it automatically reduces the case to a minimal counterexample that is actually debuggable.
  • Bind the two halves together. A failure proves the encoder and decoder disagree but not which one is wrong — which is precisely the asymmetric drift the mechanism exists to surface.

Tuning parameters

  • Generator coverage — how wide and how weird the sampled inputs are. Biasing toward edge cases finds more bugs per run but costs generation time and can obscure the common path.
  • Invariant strictness — exact equality versus equivalence-up-to-normalization versus bounded loss. Too strict and benign re-encodings fail; too loose and real loss slips through green.
  • Case count and shrink budget — more cases and deeper shrinking raise confidence and produce cleaner reproducers, at the cost of slower CI.
  • Seed policy — a fixed seed makes failures reproducible; a fresh random seed each run widens coverage over time but can look flaky when a rare case finally fires.

When it helps, and when it misleads

Its strength is reach: a single property stands in for thousands of hand-written cases and catches the asymmetric, edge-only drift between encoder and decoder that curated examples miss. Because it runs on every change, it is the standing alarm that keeps the round trip aligned as the code evolves.

Its sharpest limitation is that it proves consistency, not correctness. An encoder and decoder that are wrong in the same way — both mis-handle a field identically — will round-trip perfectly and pass forever; the test cannot see a self-consistent lie.[1] It is also only as good as its generator: an input class the generator never produces is a class never tested. The classic misuse is loosening the invariant until the suite goes green in order to ship, which converts a real defect into a passing test. The discipline is to pin the invariant to what the task genuinely requires and to pair the property with an external oracle — a specification or Golden Test Vectors — so that absolute correctness, not just internal agreement, is anchored.

How it implements the components

Round-Trip Property Test realizes the verification side of the archetype — the components a generative test can operate, not the ones it merely exercises:

  • round_trip_test_set — it is a round-trip test set, but generated on demand across a sampled space rather than enumerated by hand.
  • fidelity_invariants — the asserted property is the machine-checkable statement of the fidelity invariants: what must survive encode then decode.

It does not pin concrete expected outputs — that anchoring is Golden Test Vectors; it does not define or implement the encoder and decoder it runs (that is the Parser/Emitter Pair); and it does not detect in-transit corruption (that is Checksum or Hash Validation).

  • Instantiates: Round-Trip Code Alignment — supplies the generative fidelity check that keeps encoder and decoder aligned as the code changes.
  • Consumes: the Parser/Emitter Pair it exercises, and Codec Specification, which supplies the invariants it asserts.
  • Sibling mechanisms: Golden Test Vectors · Codec Specification · Canonicalization Rule · Decode Error Taxonomy · Parser/Emitter Pair · Checksum or Hash Validation · Compatibility Matrix · Schema Registry · Lossy Compression Profile · Migration Adapter · Version Negotiation Handshake

Notes

Self-consistency is not correctness. Because the property only checks the encoder against its own decoder, a defect shared by both survives every run. Treat this mechanism as the dense half of a testing pair — it explores enormous input variety cheaply — and let a sparse external oracle (golden vectors or a reference implementation) supply the ground truth it structurally cannot.

References

[1] Property-based testing — the practice of specifying a general property and checking it against automatically generated inputs, with automatic shrinking of counterexamples (the QuickCheck lineage; Hypothesis is a widely used modern implementation). The round-trip property decode(encode(x)) = x is its textbook example.