TL;DR

Instead of a handful of example tests, describe the rule and let a generator throw hundreds of cases at it. For pure logic — conversions, filters, gates — this finds edge cases you would never think to write by hand.

I used Vitest + fast-check for this.

Describe the rule, not the examples

A property is a statement that should hold for all inputs:

  • “Round-trip a converted value and you get the original back within tolerance.”
  • “The filter returns exactly the items that pass all gates — no more, no fewer.”

fast-check generates hundreds of inputs trying to break that statement, and shrinks any failure down to the smallest case that still fails.

What it caught that examples didn’t

Two real bugs I would not have written a test for:

  • An inherited object-property key slipping through a lookup — a case that only shows up with the right generated object shape.
  • A numeric tolerance that was too tight, which example values happened to satisfy but generated ones did not.

Keep the logic pure

Put the logic in a pure, DOM-free module so both the app and the tests import the same code. If a rule needs the DOM to run, it’s too tangled to test this way — and probably too tangled to trust.

Key lessons

  • Properties describe intent; examples describe instances. Intent finds more.
  • Generated + shrunk failures hand you the minimal reproduction for free.
  • Pure modules are the unit that property-based testing rewards.

The one-line version

Don’t test the examples you thought of — test the rule, and let the machine find the examples you didn’t.