This post is part of a series of posts on implementing a minimal version of QuickCheck from scratch. The source code is available on GitHub.
In QuickCheck the programmer writes assertions about logical properties that a function should fulfill.
List.rev as an example, which is a function that returns a new list with the elements in reverse order:
One logical property of
List.revtwice must return the elements in the original order.
Here’s an example assertion:
A generic assertion
In the previous example
List.rev was tested against an example value
[1; 2; 3]. To make the assertion generic, the example value can be parameterized as any list:
QuickCheck will then attempt to generate a test case that falsifies the assertion. In order to do that, it needs to know which generator to use, to feed
xs with random values.
A generator for lists of integers
xs need to be generated by a generator. The following one is for lists of type integer:
Every possible value generated by the
g generator must now be supplied to the assertion, as shown below:
But what is
forAll? This comes from predicate logic and essentially means that the assertion holds for all possible values generated by
In its simplest form,
Property is a generator of
Gen<Result>)1, which means we can easily try out a Property just as we would do with a generator.
Try out (see it pass)
Using only the
Property modules, the assertion can be turned into a property and sampled:
The assertion holds for all cases, which means the ‘test’ passes.
Try out (see it fail)
To see the ‘test’ fail, the assertion can be changed from
and run again:
The test now fails. — But, there’s some noise in the results; the counter-example isn’t obvious.
It’d be wise if we can clean the noise by trying to report back the minimal counter-example. This process is called shrinking and will be shown over the next post(s).