Skip to content

// Testing · 08

Property-Based Testing

Few hand examplesleads toshrink on failRandom inputsleads toInvariant holds

Assert a rule that must always hold, across many generated inputs.

How it fits together

Property-Based Testing structure«client»Random generatordecimal, 0 to 1e12100 cases per runPropertyParse(Format(x)) == xholds for every xHolds for all casesrun completenothing to reportCounterexamplex = 0.005shrunk from 3812.40171 generate x2 all hold2 violated
uses / callsfails / error path· numbered steps replay in order
  • Random generator uses / calls Property (1 generate x)
  • Property uses / calls Holds for all cases (2 all hold)
  • Property fails / error path Counterexample (2 violated)

Spot it

  • Edge cases keep appearing after release
  • Hand-picked examples miss empty or huge inputs
  • Round-trip bugs in parsing or serialising

It costs

Good properties take thought, and failures must be read as data, not as examples.

In code

using FsCheck.Xunit;using Shouldly;​public sealed class MoneyFormatTests{    [Property(MaxTest = 500)]    public void Parse_AfterFormat_ReturnsSameCents(long cents)    {        // Arrange        var text = Money.Format(cents);​        // Act        var parsed = Money.Parse(text);​        // Assert        parsed.ShouldBe(cents);    }}
Go deeperThe full explanation and what to ask for · 1 min readExpand

An example test says: for this input, expect this output. A property says: for every input in this class, this relation holds. The framework generates hundreds of inputs and tries to falsify the property, so you state the rule rather than guess the cases. Good properties are invariants (sorting keeps the length), round trips (decode of encode returns the original) and oracles (a fast version agrees with a slow, obvious one).

When a case fails, shrinking reduces it to a smaller counterexample: a 40-element list with a bug becomes the two-element list that still fails. That minimal case is usually the bug itself. Keep the seed so a failure can be reproduced, and save important shrunk cases as plain example tests so they never regress.

The trap is a property that restates the implementation, so the test and the code are wrong together. Generators must cover the real domain: an unconstrained text generator tests your parser on garbage, not on the input you receive. When NOT to: a rule with one or two meaningful cases, or one whose expected output is hard to state without rebuilding the code, is clearer as an example.

Ask for it

  • property
  • generator
  • invariant
  • shrinking
  • round-trip property
  • test oracle

Add property tests for the Money parser: for any generated amount of cents, Format then Parse returns the same cents, and Parse never throws on arbitrary text. Keep the five example tests as regression cases. Report the shrunk input for any failure.