Property-based testing checks whether general invariants hold across generated inputs instead of asserting only a few hand-picked results. It is usually a black-box form of functional testing: define the behavior that must always hold, generate cases, and let failures challenge that claim. section 15.1 section 15.1.2
The technique fits small, pure algorithms especially well. A pure algorithm can be treated as a data transformation with inherent mathematical or behavioral properties; generated inputs cover many cases that would be tedious to enumerate manually. When a property fails, the framework should shrink the failing value toward a minimal counterexample. section 15.1.2 section 15.3
Checking a squaring property
Rather than list expected outputs, assert that a squaring function returns x * x for generated Integer values. This checks a class of values, but not every value: ordinary generation concentrates on reasonably small or edge-range inputs. A malformed implementation that fails only beyond 1,000,000 can still pass 100 tests, so generation limits and success counts must match the risk being tested. section 15.1.2 section 15.1.2
Generated data must also respect the domain. For a configurable labyrinth generator, choose arbitrary dimensions, discard values outside the permitted range, run the scenario, and assert that the resulting labyrinth has valid bounds. This tests the parameterized generator rather than merely repeating a random action several times. section 15.1.3 section 15.1.3
Listing 15.5 Adjusting the test data generator
...
$ monadicIO $ do
x :: Int <- pick arbitrary #1
y :: Int <- pick arbitrary
pre (x >= 4 && x <= 10 && y >= 4 && y <= 10) #2
let action = generateLabyrinth (x, y) 3 3 #3
eLab <- run (try (runLangL coreRuntime action)) #4
case eLab of #5
Left (err :: SomeException) -> assert False
Right lab -> do
let (x, y) = getBounds lab
assert (x * y >= 16 && x * y <= 100)
Shows arbitrary dimension selection, preconditions that constrain valid bounds, scenario execution, and an assertion over the generated labyrinth. section 15.1.3
Effectful scenarios require extra setup. A property can run IO actions, create a runtime before each test, interpret the scenario, capture exceptions, and assert conditions on the result. This is workable for a free-monadic scenario, though property-based testing remains less naturally suited to IO-bound code than to compact pure logic. section 15.1.3 section 15.3
Use property-based tests effectively by:
- Stating an invariant or contract, not just a set of expected examples. section 15.1.2
- Generating inputs broadly enough to expose meaningful failures, while recognizing that finite runs cannot prove the absence of subtle bugs. section 15.1.2
- Constraining generated inputs to values accepted by the logic under test. section 15.1.3
- Treating failures as counterexamples and using shrinking to make them easier to diagnose. section 15.1.2







