Example-based tests check the cases somebody thought of, and the bugs are in the cases nobody thought of — which is a machine’s job.
public function testMoneyAdditionIsAssociative(): void
{
$this->forAll(Generatorchoose(0, 1000000),
Generatorchoose(0, 1000000),
Generatorchoose(0, 1000000))
->then(function (int $a, int $b, int $c) {
$m = fn(int $c) => new Money($c, 'GBP');
$this->assertEquals(
$m($a)->add($m($b))->add($m($c)),
$m($a)->add($m($b)->add($m($c)))
);
});
}
Finding the property is the hard part and is where the value is: associativity, round-tripping through serialisation, and invariants that must hold for any input are the three that come up. The shrinking is what makes a failure usable — the library reduces a failing input to the smallest one that still fails, which is usually a boundary value. It suits value objects and parsers and is unhelpful for anything with a database in it.