Lecture 10 — Property-based testing
Introduction
Designing, implementing and reasoning about large systems requires
abstraction.
We treat a component as a black box (hiding the implementation) and are
only interested in its behaviour (external interface): what does it
require, what does it guarantee.
Assumption: components adhere to their specification.
Property-based testing: we specify (propose, hypothesise) a property of a
component, and the tool tries to falsify it (using randomness).
A "side effect" of testing in this style is that the specification of a
component is made explicit (a set of properties).
1 · Properties
What is a property?
In property-based testing, a property is a falsifiable statement about
the behaviour of a component. We express this statement in the host
language (via an embedded domain-specific language).
Two non-examples:
- Every accepted job eventually completes. No finite test can falsify it.
-
The system is reliable when operated correctly. What counts as a
failure is not defined.
Say that we are testing a sorting function. How can we conclude that it is
implemented incorrectly? What observation would falsify the claim that it
is implemented correctly?
-
Property: for all
xs : 'a list, List.sort xs is a permutation of
xs. Falsification: a list ys (a counterexample) such that
List.sort ys is not a permutation of ys.
-
Property: for all
xs : 'a list, List.sort xs is non-decreasing.
Falsification: a list ys such that List.sort ys is not
non-decreasing.
Simple properties
Given a function f : 'a -> 'b, a simple property about f is a
predicate on 'a (a function of type 'a -> bool).
For example, with List.rev : 'a list -> 'a list as f, what property
does this predicate of type 'a list -> bool express?
let p1 xs = List.length (List.rev xs) = List.length xs
let p1Example = p1 [1; 2; 3]
The body checks that the length of the reversal of xs is the length of
xs: reversing a list preserves the length.
Concrete example
List.rev is its own inverse. FsCheck is a NuGet package; a script gets
it with one directive (the first run needs the network, later runs use the
package cache):
#r "nuget: FsCheck, 3.6.0"
open FsCheck
open FsCheck.FSharp
let revRevIsId (xs: int list) = List.rev (List.rev xs) = xs
FsCheck tries to falsify this property (find a counterexample). It
generates a list ys : int list. If revRevIsId ys = false, it has found
a counterexample and stops; otherwise it continues, by default for up to
100 tests.
In F# Interactive, Check.Quick revRevIsId runs the check. Every run of
Check.Quick uses a fresh random seed. These notes fix the seed, so that
the page shows the same run every time; Check.One (seeded, p) is
Check.Quick p with a fixed seed, and your own runs will differ:
let seeded = Config.Quick.WithReplay (Some { Rnd = Rnd (2026UL, 11UL); Size = None })
Check.One (seeded, revRevIsId)
What inputs are generated?
Check.Verbose also prints every generated input. The first five tests:
let seededVerbose =
Config.Verbose
.WithReplay(Some { Rnd = Rnd (2026UL, 11UL); Size = None })
.WithMaxTest(5)
.WithEndSize 8
Check.One (seededVerbose, revRevIsId)
0:
[]
1:
[]
2:
[-4; 0; 0; 0; 0]
3:
[]
4:
[0; -2; 0; -1]
Ok, passed 5 tests.
|
Failing property
Is the reversal of xs @ ys related to the reversal of xs and ys?
let revAppendWrong (xs: int list) (ys: int list) =
List.rev (xs @ ys) = List.rev xs @ List.rev ys
Check.One (seeded, revAppendWrong)
Falsifiable, after 2 tests (2 shrinks) (2026,11).
Last step was invoked with size of 3 and seed of (6705859008916805902,1535923343514018517).
Replay directly at failing step with (6705859008916805902,1535923343514018517,3).
Original:
[-2; 0]
[-1]
Shrunk:
[0]
[1]
with exception:
System.Exception: Expected true, got false.
|
Falsified: [0] and [1] is a counterexample. The halves swap:
let revAppend (xs: int list) (ys: int list) =
List.rev (xs @ ys) = List.rev ys @ List.rev xs
Check.One (seeded, revAppend)
Broken implementation
NonNegativeInt is a single-constructor union provided by FsCheck: its
values are the non-negative integers, so the type of the parameter keeps
the input in range.
let rec fact n =
match n with
| 0 -> 1
| 7 -> fact 6
| n -> n * fact (n - 1)
let factStep (NonNegativeInt n) = fact (n + 1) = (n + 1) * fact n
Check.One (seeded, factStep)
Falsifiable, after 7 tests (0 shrinks) (2026,11).
Last step was invoked with size of 8 and seed of (7552811758107635607,742021167357823125).
Replay directly at failing step with (7552811758107635607,742021167357823125,8).
Original:
NonNegativeInt 6
with exception:
System.Exception: Expected true, got false.
|
While fact n computes an incorrect result when n >= 7, factStep only
fails on NonNegativeInt 6: for larger n, both sides are wrong by the
same factor.
When are two lists equal?
revRevIsId passed 100 tests: FsCheck generated 100 values
ys : int list, and for all of them List.rev (List.rev ys) = ys
evaluated to true.
Lists are polymorphic in the type of their elements. Reversing a list of,
say, strings should behave the same as reversing a list of integers. Can
we conclude that List.rev (List.rev xs) = xs for all lists xs?
let revRevIsIdFloat (xs: float list) = List.rev (List.rev xs) = xs
Check.One (seeded, revRevIsIdFloat)
Falsifiable, after 16 tests (11 shrinks) (2026,11).
Last step was invoked with size of 17 and seed of (8531533158813906174,8482786554965388143).
Replay directly at failing step with (8531533158813906174,8482786554965388143,17).
Original:
[-0.0; 0.0; 0.0; 0.0; -1.242624215; nan; -0.0; nan; 0.2610306871; 0.0;
-4.97824528; -0.0]
Shrunk:
[nan]
with exception:
System.Exception: Expected true, got false.
|
No. The shrunk counterexample is [nan], and nan is not equal to
itself:
let nanIsNan = nan = nan
2 · Shrinking and replay
Counterexamples
By default, Check.Quick starts from "small" inputs. If the generated
input is not a counterexample, then the next input may be slightly
"larger".
Since the search for counterexamples (test data generation) is not
exhaustive, the first counterexample we find might not be the best
explanation of the root cause.
Once FsCheck has found a counterexample, it tries to find a "smaller"
counterexample by "shrinking" the current counterexample. A "smaller"
counterexample hopefully is a better explanation.
Shrinking a value
To shrink a value x : 'a means to propose other candidate values of type
'a that we consider to be "smaller" than x. This mechanism is a
function of type 'a -> 'a seq, the "shrinker".
If the first proposed value is also a counterexample, FsCheck recursively
shrinks that. Otherwise it considers the next proposed value.
FsCheck's arbitrary for int carries its shrinker. What are some values
"smaller" than 10 that we could try?
let intArb = ArbMap.arbitrary<int> ArbMap.defaults
let shrink10 = intArb.Shrinker 10 |> Seq.toList
Suppose that 0 is not a counterexample but 5 is. Then FsCheck shrinks
5:
let shrink5 = intArb.Shrinker 5 |> Seq.toList
Shrinking a list
A list shrinks in two ways: shrink the data structure itself, or shrink
the values in it.
let intListArb : Arbitrary<int list> = ArbMap.arbitrary ArbMap.defaults
let shrink321 = intListArb.Shrinker [3; 2; 1] |> Seq.toList
[[2; 1]; [3; 1]; [3; 2]; [0; 2; 1]; [2; 2; 1]; [3; 0; 1]; [3; 1; 1]; [3; 2; 0]]
|
The first three candidates are the original list with one element left
out. The rest are lists that are otherwise exactly the same as the
original except that one element has been replaced with a "smaller"
element:
let shrink3 = intArb.Shrinker 3 |> Seq.toList
let shrink2 = intArb.Shrinker 2 |> Seq.toList
Shrinking example
WithEveryShrink directs FsCheck to print every successful shrinking
step. Below, the printed lists are aligned, so that the gap in each line
shows the element that step left out:
Check.One (seeded.WithEveryShrink (sprintf "%A\n"), revRevIsIdFloat)
[[0.0; 0.0; 0.0; -1.242624215; nan; -0.0; nan; 0.2610306871; 0.0; -4.97824528; -0.0]]
[[ 0.0; 0.0; -1.242624215; nan; -0.0; nan; 0.2610306871; 0.0; -4.97824528; -0.0]]
[[ 0.0; -1.242624215; nan; -0.0; nan; 0.2610306871; 0.0; -4.97824528; -0.0]]
[[ -1.242624215; nan; -0.0; nan; 0.2610306871; 0.0; -4.97824528; -0.0]]
[[ nan; -0.0; nan; 0.2610306871; 0.0; -4.97824528; -0.0]]
[[ -0.0; nan; 0.2610306871; 0.0; -4.97824528; -0.0]]
[[ nan; 0.2610306871; 0.0; -4.97824528; -0.0]]
[[ nan; 0.0; -4.97824528; -0.0]]
[[ nan; -4.97824528; -0.0]]
[[ nan; -0.0]]
[[ nan ]]
Falsifiable, after 16 tests (11 shrinks) (2026,11).
Last step was invoked with size of 17 and seed of (8531533158813906174,8482786554965388143).
Replay directly at failing step with (8531533158813906174,8482786554965388143,17).
Original:
[-0.0; 0.0; 0.0; 0.0; -1.242624215; nan; -0.0; nan; 0.2610306871; 0.0;
-4.97824528; -0.0]
Shrunk:
[nan]
with exception:
System.Exception: Expected true, got false.
|
Each step leaves out one element and the property still fails, until only
nan is left.
Remove from BST
From Lecture 6, the tree, its insert (here insertTree) and
inOrder, and from Lecture 9 build:
type 'a BinTree = Empty
| Node of 'a BinTree * 'a * 'a BinTree
let rec insertTree x t =
match t with
| Empty -> Node (Empty, x, Empty)
| Node (l, y, r) ->
match compare x y with
| 0 -> t
| c when c < 0 -> Node (insertTree x l, y, r)
| _ -> Node (l, y, insertTree x r)
let rec inOrder t =
match t with
| Empty -> []
| Node (l, x, r) ->
inOrder l @ [x] @ inOrder r
let build xs = List.fold (fun acc x -> insertTree x acc) Empty xs
build xs is a search tree for any list xs. This remove contains a
bug: at the value, it keeps only the left subtree.
let rec remove x t =
match t with
| Empty -> Empty
| Node (l, y, r) ->
if x < y then Node (remove x l, y, r)
elif x > y then Node (l, y, remove x r)
else l
Removing x must keep every other value:
let removeKeepsOthers (x: int) (xs: int list) =
let t = build xs
inOrder (remove x t) = List.filter (fun y -> y <> x) (inOrder t)
Check.One (seeded, removeKeepsOthers)
Falsifiable, after 41 tests (13 shrinks) (2026,11).
Last step was invoked with size of 42 and seed of (1349500317379569017,699349187029105039).
Replay directly at failing step with (1349500317379569017,699349187029105039,42).
Original:
1
[0; 0; -1; 1; 0; 1; 0; 29; 0]
Shrunk:
0
[0; 1]
with exception:
System.Exception: Expected true, got false.
|
A shrunk counterexample
The original list gives the tree on the left; the shrunk list [0; 1]
gives the tree on the right.
Removing 0 from the shrunk tree also drops 1: the result is the left
subtree, which is Empty. The shrunk counterexample directly points to
the issue.
Reading a report
The report on revRevIsIdFloat again:
Check.One (seeded, revRevIsIdFloat)
Falsifiable, after 16 tests (11 shrinks) (2026,11).
Last step was invoked with size of 17 and seed of (8531533158813906174,8482786554965388143).
Replay directly at failing step with (8531533158813906174,8482786554965388143,17).
Original:
[-0.0; 0.0; 0.0; 0.0; -1.242624215; nan; -0.0; nan; 0.2610306871; 0.0;
-4.97824528; -0.0]
Shrunk:
[nan]
with exception:
System.Exception: Expected true, got false.
|
-
Falsifiable, after 16 tests: the sixteenth input that was generated
falsified the property;
(2026,11) is the seed of the whole run.
- 11 shrinks: FsCheck found 11 smaller inputs, each still failing.
- Last step: the seed and size of the failing test.
-
Replay directly: the seed and size that regenerate exactly that
input.
- Original: the first input that failed, one line per parameter.
- Shrunk: the smallest failing input found.
Replay
The report printed the seed and size of the failing step. A configuration
with that seed and size regenerates the same input:
let failingSeed = Rnd (8531533158813906174UL, 8482786554965388143UL)
let replayFailure =
Config.Quick.WithReplay (Some { Rnd = failingSeed; Size = Some 17 })
Check.One (replayFailure, revRevIsIdFloat)
Falsifiable, after 1 test (11 shrinks) (8531533158813906174,8482786554965388143).
Last step was invoked with size of 17 and seed of (8531533158813906174,8482786554965388143):
Original:
[-0.0; 0.0; 0.0; 0.0; -1.242624215; nan; -0.0; nan; 0.2610306871; 0.0;
-4.97824528; -0.0]
Shrunk:
[nan]
with exception:
System.Exception: Expected true, got false.
|
We can reproduce the exact same search that found the counterexample.
3 · More on properties
Conditional properties
The properties so far are of the form \(\forall (x : X), P(x)\): we require
every possible value \(x : X\) to have the property \(P\).
Sometimes we want to talk about a certain subset of the values \(x : X\).
For example, we may want to say that those values \(x : X\) that satisfy
the property \(P\) must satisfy the property \(Q\):
\(\forall (x : X), P(x) \implies Q(x)\)
For a particular x, with properties p and q, we could say
if p x then q x else true. The FsCheck syntax for such properties is
precondition ==> property.
Conditional properties (ctd.)
The following property should check that, for non-empty lists, the head
element of the reversed list is the last element of the original list:
Check.One (seeded, fun (xs: int list) ->
xs <> [] ==> (List.head (List.rev xs) = List.last xs))
Falsifiable, after 1 test (0 shrinks) (2026,11).
Last step was invoked with size of 2 and seed of (9642087697066849451,7737682524261845263):
Original:
[]
with exception:
System.ArgumentException: The input list was empty. (Parameter 'list')
|
What went wrong? Arguments are evaluated before the function body. This
applies to ==> as well, in particular also when xs = [].
Solution: wrap the property after ==> in lazy, so that it is
evaluated only when its result is actually needed:
let headRevIsLast (xs: int list) =
xs <> [] ==> lazy (List.head (List.rev xs) = List.last xs)
Check.One (seeded, headRevIsLast)
headRevIsLast passes 100 tests. Note its type: int list -> Property,
not int list -> bool.
How many tests were useful?
let palindrome xs = List.rev xs = xs
let headLastPalindrome (xs : int list) =
(xs <> [] && palindrome xs) ==> lazy (List.head xs = List.last xs)
Check.One (seeded, headLastPalindrome)
Arguments exhausted after 9 tests.
|
FsCheck only managed to (non-trivially) test the property a few times.
A "trivial" test of p ==> q: if p is false, then the whole property is
true no matter what q is. We care about the cases where q is tested.
FsCheck tried to test the property 100 times, but most of the generated
lists were not palindromes and did not count. There is a limit on how
many generated inputs can be rejected:
let maxRejected = Config.Quick.MaxRejected
Ordered lists
let rec ordered (f : 'a -> 'a -> bool) (xs : 'a list) : bool =
match xs with
| [] | [ _ ] -> true
| x :: y :: xs -> f x y && ordered f (y :: xs)
let revFlipsOrder (xs : int list) =
ordered (<=) xs ==> lazy (ordered (>=) (List.rev xs))
Check.One (seeded, revFlipsOrder)
Arguments exhausted after 25 tests.
|
Randomly generated lists are not likely to be ordered. Without a
precondition, both of these pass:
let sortIsOrdered (xs : int list) = ordered (<=) (List.sort xs)
let revFlipsOrder2 (xs : int list) = ordered (>=) (List.rev (List.sort xs))
Check.One (seeded, sortIsOrdered)
Check.One (seeded, revFlipsOrder2)
Ok, passed 100 tests.
Ok, passed 100 tests.
|
Insert into ordered list
let rec insert ord x ys =
match ys with
| [] -> [ x ]
| y :: ys -> if ord x y then x :: y :: ys else y :: insert ord x ys
let insertKeepsOrder (x: int) xs =
ordered (<=) xs ==> lazy (ordered (<=) (insert (<=) x xs))
Check.One (seeded, insertKeepsOrder)
Arguments exhausted after 20 tests.
|
Prop.trivial marks the trivial tests, here the empty lists:
let insertTrivial (x: int) xs =
ordered (<=) xs ==> lazy (ordered (<=) (insert (<=) x xs))
|> Prop.trivial (List.length xs = 0)
Check.One (seeded, insertTrivial)
Arguments exhausted after 20 tests (20% trivial).
|
The percentage is out of the successful tests (the generated list xs
was ordered, so the precondition was satisfied): in that share of them,
xs was empty.
Classify generated data
Prop.classify counts the tests in a named class:
let insertClassify (x: int) xs =
ordered (<=) xs ==> lazy (ordered (<=) (insert (<=) x xs))
|> Prop.classify (ordered (<=) (x :: xs)) "at-head"
|> Prop.classify (ordered (<=) (xs @ [ x ])) "at-last"
Check.One (seeded, insertClassify)
Arguments exhausted after 20 tests.
40% at-last.
40% at-head.
20% at-last, at-head.
|
A test can be in several classes. "at-head" counts the ordered lists
where adding the generated element at the beginning gives an ordered
list; "at-last" those where adding it at the end does.
Categorise generated data
Prop.collect counts the tests per value, here per length of xs:
let insertCollect (x: int) xs =
ordered (<=) xs ==> lazy (ordered (<=) (insert (<=) x xs))
|> Prop.collect (List.length xs)
Check.One (seeded, insertCollect)
Arguments exhausted after 20 tests.
60% 1.
20% 2.
20% 0.
|
Conjunctions, disjunctions, labels
We can form compound properties (conjunction p .&. q, disjunction
p .|. q) and label the parts so that we know which of them failed:
let insertBothHeadTail (x: int) (xs : int list) =
ordered (<=) xs ==>
lazy ((Prop.label "head" <| ordered (<=) (x :: xs))
.&.
(Prop.label "tail" <| ordered (<=) (xs @ [x])))
Check.One (seeded, insertBothHeadTail)
Falsifiable, after 1 test (0 shrinks) (2026,11).
Last step was invoked with size of 2 and seed of (9642087697066849451,7737682524261845263):
Label of failing property: tail
Original:
0
[1]
with exception:
System.Exception: Expected true, got false.
|
Expect an exception
Sometimes an exception is the expected result:
let expectDivideByZero x =
Prop.throws<System.DivideByZeroException, _> (lazy (1 / (x - x)))
Check.One (seeded, expectDivideByZero)
let expectDivideByZero' x =
Prop.throws<System.DivideByZeroException, _> (lazy (1 / (x - 2)))
Check.One (seeded, expectDivideByZero')
Falsifiable, after 1 test (0 shrinks) (2026,11).
Last step was invoked with size of 2 and seed of (9642087697066849451,7737682524261845263):
Original:
0
with exception:
System.Exception: Expected exception, none was thrown
|
4 · Generators
What are generators?
A generator of values of type X is a description of how to generate
values of type X.
We can think of Gen<'a>, a generator of values of type 'a, as a
function of type
where the first argument bounds the size of the value to be generated and
the second argument is a source of "randomness".
Why would we want to define our own generators and not use the default
ones? Recall the properties about ordered lists. We can define a
generator that is much more likely to generate an ordered list. This
allows us to test such properties much more thoroughly.
FsCheck provides combinators for building complex generators from simpler
ones.
Basic generators
Gen.constant ignores the size and randomness parameters and always
generates the same value. As a function of type
int -> Randomness -> 'a, think of it as fun s r -> 5.
let genFive : Gen<int> = Gen.constant 5
A key building block is Gen.choose : int * int -> Gen<int>. It ignores
the size but uses randomness to pick a value from the given interval
(with uniform distribution):
let smallPositive : Gen<int> = Gen.choose (1, 10)
Sample
As part of developing and testing a generator we often want to sample it
to see what kind of data it generates. Gen.sample : int -> Gen<'a> -> 'a array
takes the number of samples to produce and the generator to sample. These
notes use Gen.sampleWithSeed (a seed, a size and the number of samples)
to keep the page fixed:
let seed = Rnd (2026UL, 11UL)
let smallPositiveSample = Gen.sampleWithSeed seed 10 5 smallPositive
Gen.sample is not something to define a generator with; use it only to
see what your generator would produce. Its result is not a generator,
just an array. We build generators from other generators so that when the
generator runs, the size and randomness arguments are propagated
appropriately.
Gen.map
If we know how to generate values of type 'a and how to turn an 'a
into a 'b, then we can generate values of type 'b:
Gen.map : ('a -> 'b) -> Gen<'a> -> Gen<'b>.
let chooseFromList (xs: 'a list) : Gen<'a> =
let idxGen = Gen.choose (0, List.length xs - 1)
Gen.map (fun i -> List.item i xs) idxGen
Compare with Gen.elements : 'a seq -> Gen<'a>:
let fromListSample = Gen.sampleWithSeed seed 10 10 (chooseFromList [50 .. 100])
let elementsSample = Gen.sampleWithSeed seed 10 10 (Gen.elements [50 .. 100])
[|78; 53; 56; 67; 56; 79; 57; 76; 92; 67|]
|
[|78; 53; 56; 67; 56; 79; 57; 76; 92; 67|]
|
With the same seed, the two give the same sample.
Gen.bind
Gen.bind : ('a -> Gen<'b>) -> Gen<'a> -> Gen<'b> generates an 'a, then
takes the generator of 'b-s for that particular 'a and generates a
'b. Which generator of 'b-s is used depends on the 'a that was
generated (sequencing).
How do we generate a pair of integers where the first one is smaller than
the second? Generate the first one, and then use a generator that can
only generate larger numbers:
let larger i : Gen<int * int> =
Gen.choose (i + 1, 100)
|> Gen.map (fun j -> (i, j))
let ordPair : Gen<int * int> =
Gen.choose (0, 99)
|> Gen.bind larger
let ordPairSample = Gen.sampleWithSeed seed 10 5 ordPair
[|(67, 92); (12, 36); (60, 86); (84, 85); (3, 85)|]
|
Choosing a generator
Gen.oneof : Gen<'a> seq -> Gen<'a> first picks a generator, and that
generator then generates the value. So
Gen.oneof [ Gen.constant true; Gen.constant false ] is a Gen<bool>
that, every time it needs to generate a value, first picks a generator.
Gen.frequency : (int * Gen<'a>) seq -> Gen<'a> controls the
distribution: it picks a generator from a list of generators, weighting
the probability of each by the given factor.
let boolGenOptimist = Gen.frequency [ (2, Gen.constant true); (1, Gen.constant false) ]
let optimistTrues =
Gen.sampleWithSeed seed 10 100 boolGenOptimist |> Array.filter id |> Array.length
About two thirds of 100 samples are true.
Sized generators
Gen.sized : (int -> Gen<'a>) -> Gen<'a> takes a function that gives a
generator for every possible size s : int. For example, natural numbers
not larger than the given size:
let sizedNats s = Gen.choose (0, s)
We have a generator for every s : int. Gen.sized turns it into a
generator of natural numbers that respects the size parameter:
let naturalGen = Gen.sized sizedNats
let naturalsAt10 = Gen.sampleWithSeed seed 10 10 naturalGen
let naturalsAt20 = Gen.sampleWithSeed seed 20 10 naturalGen
let naturalsAt100 = Gen.sampleWithSeed seed 100 10 naturalGen
[|4; 2; 8; 1; 4; 5; 8; 2; 7; 8|]
|
[|1; 0; 15; 5; 0; 11; 7; 14; 9; 20|]
|
[|94; 23; 16; 81; 89; 99; 77; 90; 59; 83|]
|
Arbitrary
An Arbitrary<'a> is the combination of a generator of 'a-s and a
shrinker of 'a-s:
Arb.fromGenShrink : Gen<'a> * ('a -> 'a seq) -> Arbitrary<'a>.
This is what FsCheck needs to try to falsify a property 'a -> bool:
generate an arbitrary value of type 'a, and if it is a counterexample,
try to shrink it. Custom generators should preferably also respect the
size parameter.
-
intArb of section 2 is FsCheck's arbitrary for int:
intArb.Generator is a Gen<int>, and intArb.Shrinker an
int -> int seq.
-
Arb.fromGen makes a new arbitrary from a given generator, with an
empty shrinker.
Arb.fromGenShrink makes one from a given generator and shrinker.
Arb.filter filters the generator and the shrinker.
-
Arb.mapFilter maps and filters the generator, and filters the
shrinker.
Quantified properties
Prop.forAll : Arbitrary<'a> -> ('a -> 'b) -> Property. Thus
Prop.forAll arb p requires that every value a from arb satisfies the
property p. By supplying a custom arbitrary we control the distribution
of the test data:
let primeIsOdd =
Prop.forAll (Arb.fromGen (chooseFromList [2; 3; 5; 7; 11; 13; 17; 19]))
(fun p -> p % 2 = 1)
Check.One (seeded, primeIsOdd)
Falsifiable, after 4 tests (0 shrinks) (2026,11).
Last step was invoked with size of 5 and seed of (12252048355222541855,11250175973109801033).
Replay directly at failing step with (12252048355222541855,11250175973109801033,5).
Original:
2
with exception:
System.Exception: Expected true, got false.
|
Arb.fromGen has an empty shrinker, so there were no shrinks.
Generating ordered lists
One way to generate ordered lists, without relying on filtering, starts
from intListArb of section 2:
let orderedLists : Arbitrary<int list> = Arb.mapFilter List.sort (ordered (<=)) intListArb
Now we can say that for all x and for all ordered lists xs,
insert x xs is an ordered list. We do not need a conditional property
to exclude some of the generated data:
let insertKeepsOrder' x =
Prop.forAll orderedLists (fun xs -> ordered (<=) (insert (<=) x xs))
Check.One (seeded, insertKeepsOrder')
Generating recursive data
type Tree = Leaf of int
| Branch of Tree * Tree
The FsCheck documentation provides a "naive" generator. It does not work:
let rec unsafeTree () =
printfn "UNSAFE!"
Gen.oneof
[ Gen.map Leaf intArb.Generator
Gen.map2
(fun x y ->
printfn "BRANCH"
Branch(x, y))
(unsafeTree ())
(unsafeTree ()) ]
// unsafeTree () // Careful! Overflows the stack.
The call is commented out, so that the script runs to the end; remove the
// to try it. Calling unsafeTree () evaluates the arguments of Gen.oneof first,
including the two recursive calls unsafeTree (), before any tree is
generated. Each of them does the same, so UNSAFE! is printed until the
stack overflows, and BRANCH never is.
Tree generator that respects size
We define a function int -> Gen<Tree>, in other words a generator of
trees for every s : int:
let rec treeWithSize (s: int) : Gen<Tree> =
match s with
| 0 -> Gen.map Leaf intArb.Generator
| _ ->
let subTreeGen = treeWithSize (s / 2)
Gen.frequency
[ (1, Gen.map Leaf intArb.Generator)
(2, Gen.map2
(fun left right -> Branch (left, right))
subTreeGen
subTreeGen) ]
let treeGen = Gen.sized treeWithSize
let treeSample = Gen.sampleWithSeed seed 10 1 treeGen
[|Branch
(Leaf 5,
Branch
(Leaf 1, Branch (Branch (Leaf -1, Leaf 10), Branch (Leaf -1, Leaf 1))))|]
|
The values in the leaves come from intArb.Generator, in both places it
is used; another generator there controls the values generated for the
tree.
Computation expressions for generators
gen { ... } gives a more imperative notation for generators.
let! x = g binds the value generated by g to x in the rest of the
computation expression, and return v is a constant generator of v:
let ordPairGen =
gen {
let! j = Gen.choose (1, 100)
let! i = Gen.choose (0, j - 1)
return (i, j)
}
let ordPairGenSample = Gen.sampleWithSeed seed 10 10 ordPairGen
[|(20, 68); (1, 13); (12, 61); (26, 85); (2, 4); (15, 33); (0, 33); (45, 50);
(18, 20); (37, 58)|]
|
How this notation works: Lecture 11.
Patterns of properties
Where do properties come from? Some patterns recur. Each example below
passes 100 tests unless it says otherwise.
There and back
Convert a value to some other value and then convert it back. Given
f : S -> T and g : T -> S, is g (f s) = s? Is f (g t) = t?
Serialisation, parsing and conversion are examples: different
representations of the same thing.
let intRoundTrip (n: int) = int (string n) = n
Check.One (seeded, intRoundTrip)
The other direction, string (int s) = s, does not hold. "007" is a
number, but it does not come back as itself:
let back007 = string (int "007")
Maybe instead of equality we need to use some relation. For float, =
is too strict because of nan, as in section 1. The method Equals
treats nan as equal to itself:
let floatRoundTrip (x: float) = float (string x) = x
let floatRoundTripEquals (x: float) = (float (string x)).Equals x
Check.One (seeded, floatRoundTrip)
Check.One (seeded, floatRoundTripEquals)
Falsifiable, after 43 tests (0 shrinks) (2026,11).
Last step was invoked with size of 44 and seed of (8536316237019846243,5574149147119119315).
Replay directly at failing step with (8536316237019846243,5574149147119119315,44).
Original:
nan
with exception:
System.Exception: Expected true, got false.
Ok, passed 100 tests.
|
Changing the order
Changing the order of some operations should not change the result:
g (f x) = f (g x).
let left (x, y) = (x + x, y)
let right (x, y) = (x, y * 2)
let leftRight (x: int, y: int) = left (right (x, y)) = right (left (x, y))
let mapRev (xs: int list) =
List.rev (List.map ((+) 1) xs) = List.map ((+) 1) (List.rev xs)
Check.One (seeded, leftRight)
Check.One (seeded, mapRev)
Ok, passed 100 tests.
Ok, passed 100 tests.
|
Different ways of doing the same thing
Two different computations should give the same result:
let mapMap (xs: int list) =
List.map ((+) 1) (List.map (( * ) 2) xs) = List.map ((+) 1 << ( * ) 2) xs
let sumOfLengths (xs: string list) =
List.sum (List.map String.length xs) = String.length (String.concat "" xs)
Check.One (seeded, mapMap)
Check.One (seeded, sumOfLengths)
Ok, passed 100 tests.
Ok, passed 100 tests.
|
Some things stay the same
An "invariant" of a value does not change under the operation:
invariant (f x) = invariant x. The length of a list is an invariant of
List.map, and, as in section 1, of List.sort:
let mapKeepsLength (xs: int list) = List.length (List.map ((+) 1) xs) = List.length xs
let sortKeepsLength (xs: int list) = List.length (List.sort xs) = List.length xs
Check.One (seeded, mapKeepsLength)
Check.One (seeded, sortKeepsLength)
Ok, passed 100 tests.
Ok, passed 100 tests.
|
Twice is the same as once
Performing an operation twice is the same as performing it once
("idempotence"):
let distinctTwice (xs: int list) = List.distinct (List.distinct xs) = List.distinct xs
let sortTwice (xs: int list) = List.sort (List.sort xs) = List.sort xs
Check.One (seeded, distinctTwice)
Check.One (seeded, sortTwice)
Ok, passed 100 tests.
Ok, passed 100 tests.
|
Twice is the same as doing nothing
Performing an operation twice is the same as doing nothing
("involution"): revRevIsId of section 1. The identity function id
satisfies the same property, but it is not List.rev:
let idIdIsId (xs: int list) = id (id xs) = xs
Check.One (seeded, idIdIsId)
revAppend of section 1 tells them apart: id does not satisfy it.
let idAppend = id ([1] @ [2]) = id [2] @ id [1]
Look for a property that only the right function satisfies.
Compare with another implementation
The result should be the result of another implementation, for example a
simpler or slower one ("test oracle"). Insertion sort, with insert of
section 3, against List.sort:
let insertionSort xs = List.foldBack (insert (<=)) xs []
let sortsAgree (xs: int list) = insertionSort xs = List.sort xs
Check.One (seeded, sortsAgree)
Summary
- FsCheck tries to falsify a property.
- A failing input is shrunk to a small counterexample.
- Conditional properties (and filtering) require care. Measure.
- Control test data distribution with custom generators.
Homework, reading and next week
Homework 6: defence this week.
Reading:
namespace Itt8060
module Diagrams
from Itt8060
val scaled: factor: float -> Svg -> Svg
val factor: float
Multiple items
val float: value: 'T -> float (requires member op_Explicit)
--------------------
type float = System.Double
--------------------
type float<'Measure> =
float
Multiple items
union case Svg.Svg: string -> Svg
--------------------
type Svg = | Svg of string
A rendered SVG fragment (inline-embeddable, standalone-saveable).
val s: string
val widthAttr: System.Text.RegularExpressions.Regex
namespace System
namespace System.Text
namespace System.Text.RegularExpressions
Multiple items
type Regex =
interface ISerializable
new: pattern: string -> unit + 2 overloads
member Count: input: string -> int + 8 overloads
member EnumerateMatches: input: ReadOnlySpan<char> -> ValueMatchEnumerator + 4 overloads
member EnumerateSplits: input: ReadOnlySpan<char> -> ValueSplitEnumerator + 5 overloads
member GetGroupNames: unit -> string array
member GetGroupNumbers: unit -> int array
member GroupNameFromNumber: i: int -> string
member GroupNumberFromName: name: string -> int
member IsMatch: input: ReadOnlySpan<char> -> bool + 9 overloads
...
<summary>Represents an immutable regular expression.</summary>
--------------------
System.Text.RegularExpressions.Regex( pattern: string) : System.Text.RegularExpressions.Regex
System.Text.RegularExpressions.Regex( pattern: string, options: System.Text.RegularExpressions.RegexOptions) : System.Text.RegularExpressions.Regex
System.Text.RegularExpressions.Regex( pattern: string, options: System.Text.RegularExpressions.RegexOptions, matchTimeout: System.TimeSpan) : System.Text.RegularExpressions.Regex
val w: int
Multiple items
val int: value: 'T -> int (requires member op_Explicit)
--------------------
type int = int32
--------------------
type int<'Measure> =
int
System.Text.RegularExpressions.Regex.Match(input: string) : System.Text.RegularExpressions.Match
System.Text.RegularExpressions.Regex.Match(input: string, startat: int) : System.Text.RegularExpressions.Match
System.Text.RegularExpressions.Regex.Match(input: string, beginning: int, length: int) : System.Text.RegularExpressions.Match
System.Text.RegularExpressions.Regex.Replace(input: string, evaluator: System.Text.RegularExpressions.MatchEvaluator) : string
System.Text.RegularExpressions.Regex.Replace(input: string, replacement: string) : string
System.Text.RegularExpressions.Regex.Replace(input: string, evaluator: System.Text.RegularExpressions.MatchEvaluator, count: int) : string
System.Text.RegularExpressions.Regex.Replace(input: string, replacement: string, count: int) : string
System.Text.RegularExpressions.Regex.Replace(input: string, evaluator: System.Text.RegularExpressions.MatchEvaluator, count: int, startat: int) : string
System.Text.RegularExpressions.Regex.Replace(input: string, replacement: string, count: int, startat: int) : string
val saveScaled: path: string -> factor: float -> svg: Svg -> unit
val path: string
Multiple items
val string: value: 'T -> string
--------------------
type string = System.String
val svg: Svg
val save: path: string -> Svg -> unit
Save a diagram for use from slide decks: `Diagrams.save "img/list567.svg" (consCells ...)`.
Paths are relative to the lecture directory when evaluated by the build.
val p1: xs: 'a list -> bool
val xs: 'a list
Multiple items
module List
from Microsoft.FSharp.Collections
--------------------
type List<'T> =
| op_Nil
| op_ColonColon of Head: 'T * Tail: 'T list
interface IReadOnlyList<'T>
interface IReadOnlyCollection<'T>
interface IEnumerable
interface IEnumerable<'T>
member GetReverseIndex: rank: int * offset: int -> int
member GetSlice: startIndex: int option * endIndex: int option -> 'T list
static member Cons: head: 'T * tail: 'T list -> 'T list
member Head: 'T
member IsEmpty: bool
member Item: index: int -> 'T with get
...
val length: list: 'T list -> int
val rev: list: 'T list -> 'T list
val p1Example: bool
namespace FsCheck
namespace FsCheck.FSharp
val revRevIsId: xs: int list -> bool
val xs: int list
type 'T list = List<'T>
val seeded: Config
type Config =
private | Config of {| ArbMap: IArbMap; EndSize: int; Every: (int -> obj list -> string); EveryShrink: (obj list -> string); MaxRejected: int; MaxTest: int; Name: string; ParallelRunConfig: ParallelRunConfig option; QuietOnSuccess: bool; Replay: Replay option; Runner: IRunner; StartSize: int |}
member WithArbitrary: arbitrary: #Type seq -> Config
member WithEndSize: endSize: int -> Config
member WithEvery: every: (int -> obj list -> string) -> Config
member WithEveryShrink: everyShrink: (obj list -> string) -> Config
member WithMaxRejected: maxRejected: int -> Config
member WithMaxTest: maxTest: int -> Config
member WithName: name: string -> Config
member WithParallelRunConfig: config: ParallelRunConfig option -> Config
member WithQuietOnSuccess: quietOnSuccess: bool -> Config
member WithReplay: replay: Replay option -> Config
...
<summary>
For configuring a run.
</summary>
property Config.Quick: Config with get
<summary>
The quick configuration only prints a summary result at the end of the test.
</summary>
member Config.WithReplay: replay: Replay option -> Config
static member ConfigExtensions.WithReplay: config: Config * seed: uint64 * gamma: uint64 -> Config
static member ConfigExtensions.WithReplay: config: Config * seed: uint64 * gamma: uint64 * size: int -> Config
union case Option.Some: Value: 'T -> Option<'T>
Multiple items
type Rnd =
new: seed: uint64 * gamma: uint64 -> Rnd + 1 overload
val Seed: uint64
val Gamma: uint64
override ToString: unit -> string
--------------------
Rnd ()
new: seed: uint64 -> Rnd
new: seed: uint64 * gamma: uint64 -> Rnd
union case Option.None: Option<'T>
type Check =
static member All: config: Config * test: Type -> unit + 1 overload
static member Method: config: Config * methodInfo: MethodInfo * ?target: obj -> unit
static member One: config: Config * property: 'Testable -> unit + 1 overload
static member Quick: property: 'Testable -> unit + 1 overload
static member QuickAll: test: Type -> unit + 1 overload
static member QuickThrowOnFailure: property: 'Testable -> unit
static member QuickThrowOnFailureAll: test: Type -> unit + 1 overload
static member Verbose: property: 'Testable -> unit + 1 overload
static member VerboseAll: test: Type -> unit + 1 overload
static member VerboseThrowOnFailure: property: 'Testable -> unit
...
static member Check.One: config: Config * property: 'Testable -> unit
static member Check.One: name: string * config: Config * property: 'Testable -> unit
val seededVerbose: Config
property Config.Verbose: Config with get
<summary>
The verbose configuration prints each generated argument.
</summary>
val revAppendWrong: xs: int list -> ys: int list -> bool
val ys: int list
val revAppend: xs: int list -> ys: int list -> bool
val fact: n: int -> int
val n: int
val factStep: NonNegativeInt -> bool
Multiple items
union case NonNegativeInt.NonNegativeInt: int -> NonNegativeInt
--------------------
type NonNegativeInt =
| NonNegativeInt of int
override ToString: unit -> string
static member op_Explicit: NonNegativeInt -> int
member Get: int
<summary>
Represents an int >= 0
</summary>
val revRevIsIdFloat: xs: float list -> bool
val xs: float list
val nanIsNan: bool
val nan: float
val intArb: Arbitrary<int>
module ArbMap
from FsCheck.FSharp
val arbitrary: arbMap: IArbMap -> Arbitrary<'T>
val defaults: IArbMap
<summary>
The immutable default mapping from a type to Arbitrary for that type.
</summary>
val shrink10: int list
override Arbitrary.Shrinker: 'T -> 'T seq
module Seq
from Microsoft.FSharp.Collections
val toList: source: 'T seq -> 'T list
val shrink5: int list
val intListArb: Arbitrary<int list>
Multiple items
type Arbitrary<'T> =
interface IArbitrary
new: unit -> Arbitrary<'T>
abstract Shrinker: 'T -> 'T seq + 1 overload
abstract Generator: Gen<'T>
--------------------
new: unit -> Arbitrary<'T>
val shrink321: int list list
val shrink3: int list
val shrink2: int list
static member ConfigExtensions.WithEveryShrink: config: Config * everyShrink: System.Func<obj array,string> -> Config
member Config.WithEveryShrink: everyShrink: (obj list -> string) -> Config
val sprintf: format: Printf.StringFormat<'T> -> 'T
val shrinkSteps: ResizeArray<string list>
type ResizeArray<'T> = System.Collections.Generic.List<'T>
val recordShrinks: IRunner
type IRunner =
abstract OnArguments: int * obj list * (int -> obj list -> string) -> unit
abstract OnFinished: string * TestResult -> unit
abstract OnShrink: obj list * (obj list -> string) -> unit
abstract OnStartFixture: Type -> unit
<summary>
For implementing your own test runner.
</summary>
val args: obj list
System.Collections.Generic.List.Add(item: string list) : unit
val map: mapping: ('T -> 'U) -> list: 'T list -> 'U list
member Config.WithRunner: runner: IRunner -> Config
val columnsAfter: cols: int list -> prev: string list -> next: string list -> int list
val cols: int list
val prev: string list
val next: string list
val k: int
val find: predicate: ('T -> bool) -> list: 'T list -> 'T
val removeAt: index: int -> source: 'T list -> 'T list
val alignedSteps: string list
val steps: string list list
val ofSeq: source: 'T seq -> 'T list
val head: list: 'T list -> 'T
val colsPerStep: int list list
val pairwise: list: 'T list -> ('T * 'T) list
val scan<'T,'State> : folder: ('State -> 'T -> 'State) -> state: 'State -> list: 'T list -> 'State list
val widths: int array
module Array
from Microsoft.FSharp.Collections
val zeroCreate: count: int -> 'T array
val step: string list
val zip: list1: 'T1 list -> list2: 'T2 list -> ('T1 * 'T2) list
val iter2: action: ('T1 -> 'T2 -> unit) -> list1: 'T1 list -> list2: 'T2 list -> unit
val c: int
val t: string
val max: e1: 'T -> e2: 'T -> 'T (requires comparison)
property System.String.Length: int with get
val starts: int array
val scan<'T,'State> : folder: ('State -> 'T -> 'State) -> state: 'State -> array: 'T array -> 'State array
val at: int
val render: step: string list -> cols: int list -> string
val line: char array
val create: count: int -> value: 'T -> 'T array
val put: at: int -> t: string -> unit
module String
from Microsoft.FSharp.Core
val iteri: action: (int -> char -> unit) -> str: string -> unit
val i: int
val ch: char
val iteri: action: (int -> 'T -> unit) -> list: 'T list -> unit
Multiple items
type String =
interface seq<char>
interface IEnumerable
interface ICloneable
interface IComparable
interface IComparable<string>
interface IConvertible
interface IEquatable<string>
interface IParsable<string>
interface ISpanParsable<string>
new: value: nativeptr<char> -> unit + 8 overloads
...
<summary>Represents text as a sequence of UTF-16 code units.</summary>
--------------------
System.String(value: nativeptr<char>) : System.String
System.String(value: char array) : System.String
System.String(value: System.ReadOnlySpan<char>) : System.String
System.String(value: nativeptr<sbyte>) : System.String
System.String(c: char, count: int) : System.String
System.String(value: nativeptr<char>, startIndex: int, length: int) : System.String
System.String(value: char array, startIndex: int, length: int) : System.String
System.String(value: nativeptr<sbyte>, startIndex: int, length: int) : System.String
System.String(value: nativeptr<sbyte>, startIndex: int, length: int, enc: System.Text.Encoding) : System.String
val map2: mapping: ('T1 -> 'T2 -> 'U) -> list1: 'T1 list -> list2: 'T2 list -> 'U list
val iter: action: ('T -> unit) -> list: 'T list -> unit
val printfn: format: Printf.TextWriterFormat<'T> -> 'T
'a
type 'a BinTree =
| Empty
| Node of 'a BinTree * 'a * 'a BinTree
union case Tree.Node: label: string * children: Tree list -> Tree
val insertTree: x: 'a -> t: 'a BinTree -> 'a BinTree (requires comparison)
val x: 'a (requires comparison)
val t: 'a BinTree (requires comparison)
union case BinTree.Empty: 'a BinTree
union case BinTree.Node: 'a BinTree * 'a * 'a BinTree -> 'a BinTree
val l: 'a BinTree (requires comparison)
val y: 'a (requires comparison)
val r: 'a BinTree (requires comparison)
val compare: e1: 'T -> e2: 'T -> int (requires comparison)
val inOrder: t: 'a BinTree -> 'a list
val t: 'a BinTree
val l: 'a BinTree
val x: 'a
val r: 'a BinTree
val build: xs: 'a list -> 'a BinTree (requires comparison)
val xs: 'a list (requires comparison)
val fold<'T,'State> : folder: ('State -> 'T -> 'State) -> state: 'State -> list: 'T list -> 'State
val acc: 'a BinTree (requires comparison)
val remove: x: 'a -> t: 'a BinTree -> 'a BinTree (requires comparison)
val removeKeepsOthers: x: int -> xs: int list -> bool
val x: int
val t: int BinTree
val filter: predicate: ('T -> bool) -> list: 'T list -> 'T list
val y: int
val mutable counterexample: (obj list * obj list) option
type obj = System.Object
type 'T option = Option<'T>
val capture: IRunner
val result: TestResult
type TestResult =
| Passed of TestData * bool
| Failed of TestData * obj list * obj list * Outcome * Rnd * Rnd * int
| Exhausted of TestData
union case TestResult.Failed: TestData * obj list * obj list * Outcome * Rnd * Rnd * int -> TestResult
val original: obj list
val shrunk: obj list
val shape: t: 'a BinTree -> BinShape
union case BinShape.BinEmpty: BinShape
union case BinShape.BinNode: BinShape * string * BinShape -> BinShape
val treeOfArgs: args: obj list -> int BinTree
val viewBoxAttr: System.Text.RegularExpressions.Regex
val viewBoxHeight: Svg -> int
union case ResultContainer.Value: Result -> ResultContainer
val padToHeight: h: int -> Svg -> Svg
val h: int
val w: string
val figCxOriginal: Svg
val figCxShrunk: Svg
val o: Svg
val binTree: root: BinShape -> Svg
A binary tree with empty children left out (their positions are kept).
val s: Svg
val failwith: message: string -> 'T
val failingSeed: Rnd
val replayFailure: Config
val last: list: 'T list -> 'T
val upToException: config: Config -> p: 'a -> unit
val config: Config
val p: 'a
val out: System.IO.TextWriter
type Console =
static member Beep: unit -> unit + 1 overload
static member Clear: unit -> unit
static member GetCursorPosition: unit -> struct (int * int)
static member MoveBufferArea: sourceLeft: int * sourceTop: int * sourceWidth: int * sourceHeight: int * targetLeft: int * targetTop: int -> unit + 1 overload
static member OpenStandardError: unit -> Stream + 1 overload
static member OpenStandardInput: unit -> Stream + 1 overload
static member OpenStandardOutput: unit -> Stream + 1 overload
static member Read: unit -> int
static member ReadKey: unit -> ConsoleKeyInfo + 1 overload
static member ReadLine: unit -> string
...
<summary>Represents the standard input, output, and error streams for console applications. This class cannot be inherited.</summary>
property System.Console.Out: System.IO.TextWriter with get
<summary>Gets the standard output stream.</summary>
<returns>A <see cref="T:System.IO.TextWriter" /> that represents the standard output stream.</returns>
val w: System.IO.StringWriter
namespace System.IO
Multiple items
type StringWriter =
inherit TextWriter
new: unit -> unit + 3 overloads
member Close: unit -> unit
member FlushAsync: unit -> Task
member GetStringBuilder: unit -> StringBuilder
member ToString: unit -> string
member Write: value: char -> unit + 4 overloads
member WriteAsync: value: char -> Task + 4 overloads
member WriteLine: buffer: ReadOnlySpan<char> -> unit + 1 overload
member WriteLineAsync: value: char -> Task + 4 overloads
...
<summary>Implements a <see cref="T:System.IO.TextWriter" /> for writing information to a string. The information is stored in an underlying <see cref="T:System.Text.StringBuilder" />.</summary>
--------------------
System.IO.StringWriter() : System.IO.StringWriter
System.IO.StringWriter(formatProvider: System.IFormatProvider) : System.IO.StringWriter
System.IO.StringWriter(sb: System.Text.StringBuilder) : System.IO.StringWriter
System.IO.StringWriter(sb: System.Text.StringBuilder, formatProvider: System.IFormatProvider) : System.IO.StringWriter
System.Console.SetOut(newOut: System.IO.TextWriter) : unit
val lines: string array
System.IO.StringWriter.ToString() : string
val findIndex: predicate: ('T -> bool) -> array: 'T array -> int
val l: string
System.String.StartsWith(value: string) : bool
System.String.StartsWith(value: char) : bool
System.String.StartsWith(value: string, comparisonType: System.StringComparison) : bool
System.String.StartsWith(value: string, ignoreCase: bool, culture: System.Globalization.CultureInfo) : bool
val truncate: count: int -> array: 'T array -> 'T array
val concat: sep: string -> strings: string seq -> string
val printf: format: Printf.TextWriterFormat<'T> -> 'T
val headRevIsLast: xs: int list -> Property
val palindrome: xs: 'a list -> bool (requires equality)
val xs: 'a list (requires equality)
val headLastPalindrome: xs: int list -> Property
val maxRejected: int
property Config.MaxRejected: int with get
<summary>
The maximum number of tests where values are rejected, e.g. as the result of ==>
</summary>
val ordered: f: ('a -> 'a -> bool) -> xs: 'a list -> bool
val f: ('a -> 'a -> bool)
type bool = System.Boolean
val y: 'a
val revFlipsOrder: xs: int list -> Property
val sortIsOrdered: xs: int list -> bool
val sort: list: 'T list -> 'T list (requires comparison)
val revFlipsOrder2: xs: int list -> bool
val insert: ord: ('a -> 'a -> bool) -> x: 'a -> ys: 'a list -> 'a list
val ord: ('a -> 'a -> bool)
val ys: 'a list
val insertKeepsOrder: x: int -> xs: int list -> Property
val insertTrivial: x: int -> xs: int list -> Property
module Prop
from FsCheck.FSharp
<summary>
Combinators to build properties, which define the property to be tested, with some
convenience methods to investigate the generated arguments and any found counter-examples.
</summary>
val trivial: b: bool -> ('Testable -> Property)
<summary>
Count trivial cases. Test cases for which the condition is True are classified as trivial.
</summary>
val insertClassify: x: int -> xs: int list -> Property
val classify: b: bool -> name: string -> ('Testable -> Property)
<summary>
Classify test cases. Test cases satisfying the condition are assigned the classification given.
</summary>
val insertCollect: x: int -> xs: int list -> Property
val collect: v: 'CollectedValue -> ('Testable -> Property)
<summary>
Collect data values. The argument of collect is evaluated in each test case,
and the distribution of values is reported, using sprintf "%A".
</summary>
val insertBothHeadTail: x: int -> xs: int list -> Property
val label: l: string -> ('Testable -> Property)
<summary>
Add the given label to the property. The labels of a failing sub-property are displayed when it fails.
</summary>
val expectDivideByZero: x: int -> Property
val throws<'Exception,'Testable (requires 'Exception :> exn)> : p: System.Lazy<'Testable> -> Property (requires 'Exception :> exn)
<summary>
Expect exception 't when executing p. So, results in success if an exception of the given type is thrown,
and a failure otherwise.
</summary>
Multiple items
type DivideByZeroException =
inherit ArithmeticException
new: unit -> unit + 2 overloads
<summary>The exception that is thrown when there is an attempt to divide an integral or <see cref="T:System.Decimal" /> value by zero.</summary>
--------------------
System.DivideByZeroException() : System.DivideByZeroException
System.DivideByZeroException(message: string) : System.DivideByZeroException
System.DivideByZeroException(message: string, innerException: exn) : System.DivideByZeroException
val expectDivideByZero': x: int -> Property
val genFive: Gen<int>
Multiple items
module Gen
from FsCheck.FSharp
--------------------
type Gen<'T> =
private | Gen of (int -> Rnd -> struct ('T * Rnd))
interface IGen
<summary>
Generator of a random value, based on a size parameter and a randomly generated int.
</summary>
val constant: value: 'T -> Gen<'T>
<summary>
Always generate the same given value. See also fresh.
</summary>
val smallPositive: Gen<int>
val choose: l: int * h: int -> Gen<int>
<summary>
Generates ints between l and h, inclusive.
</summary>
val seed: Rnd
val smallPositiveSample: int array
val sampleWithSeed: seed: Rnd -> size: int -> nbSamples: int -> Gen<'T> -> 'T array
<summary>
Generates n values of the given size and starting with the given seed.
</summary>
val chooseFromList: xs: 'a list -> Gen<'a>
val idxGen: Gen<int>
val map: f: ('T -> 'U) -> Gen<'T> -> Gen<'U>
<summary>
Create a new generator by applying f to each value in the given generator.
</summary>
val item: index: int -> list: 'T list -> 'T
val fromListSample: int array
val elementsSample: int array
val elements: xs: 'T seq -> Gen<'T>
<summary>
Build a generator that randomly generates one of the values in the given non-empty, finite seq.
</summary>
val larger: i: int -> Gen<int * int>
val j: int
val ordPair: Gen<int * int>
val bind: k: ('T -> Gen<'U>) -> Gen<'T> -> Gen<'U>
<summary>
Creates a new generator that generates values from the source generator,
applies the function k to them, and generates values from the resulting generator.
</summary>
val ordPairSample: (int * int) array
val boolGenOptimist: Gen<bool>
val frequency: dist: (int * Gen<'T>) seq -> Gen<'T>
<summary>
Build a generator that generates a value from one of the generators in the given non-empty seq, with
given probabilities. The sum of the probabilities must be larger than zero.
</summary>
<param name="dist">Sequence of tuples where each tuple contains a weight and a generator.</param>
<exception cref="System.ArgumentException">Thrown if the sum of the probabilities is less than or equal to 0.</exception>
val optimistTrues: int
val filter: predicate: ('T -> bool) -> array: 'T array -> 'T array
val id: x: 'T -> 'T
val length: array: 'T array -> int
val sizedNats: s: int -> Gen<int>
val s: int
val naturalGen: Gen<int>
val sized: gen: (int -> Gen<'T>) -> Gen<'T>
<summary>
Obtain the current size. sized g calls g, passing it the current size as a parameter.
</summary>
val naturalsAt10: int array
val naturalsAt20: int array
val naturalsAt100: int array
val primeIsOdd: Property
val forAll: arb: Arbitrary<'Value> -> body: ('Value -> 'Testable) -> Property
<summary>
Quantified property combinator. Provide a custom test data generator to a property.
</summary>
module Arb
from FsCheck.FSharp
val fromGen: gen: Gen<'Value> -> Arbitrary<'Value>
<summary>
Construct an Arbitrary instance from a generator.
Shrink is not supported for this type.
</summary>
val p: int
val orderedLists: Arbitrary<int list>
val mapFilter: mapper: ('T -> 'T) -> pred: ('T -> bool) -> a: Arbitrary<'T> -> Arbitrary<'T>
<summary>
Return an Arbitrary instance that is a mapped and filtered version of an existing arbitrary instance.
The generator uses Gen.map with the given mapper and then Gen.where with the given predicate,
and the shrinks are filtered using Seq.filter with the given predicate.
This is sometimes useful if using just a filter would reduce the chance of getting a good value
from the generator - and you can map the value instead. E.g. PositiveInt.
</summary>
val insertKeepsOrder': x: int -> Property
type Tree =
| Leaf of int
| Branch of Tree * Tree
val unsafeTree: unit -> Gen<Tree>
val oneof: gens: Gen<'T> seq -> Gen<'T>
<summary>
Build a generator that generates a value from one of the generators in the given non-empty seq, with
equal probability.
</summary>
union case Tree.Leaf: int -> Tree
property Arbitrary.Generator: Gen<int> with get
<summary>
Returns a generator for 'a.
</summary>
val map2: f: ('a -> 'b -> 'c) -> Gen<'a> -> Gen<'b> -> Gen<'c>
<summary>
Create a new generator by applying f to each value in the given generators.
</summary>
val x: Tree
val y: Tree
union case Tree.Branch: Tree * Tree -> Tree
val treeWithSize: s: int -> Gen<Tree>
val subTreeGen: Gen<Tree>
val left: Tree
val right: Tree
val treeGen: Gen<Tree>
val treeSample: Tree array
val ordPairGen: Gen<int * int>
val gen: GenBuilder
<summary>
The computation expressions for generators: gen { ... }
</summary>
val ordPairGenSample: (int * int) array
val intRoundTrip: n: int -> bool
val back007: string
val floatRoundTrip: x: float -> bool
val x: float
val floatRoundTripEquals: x: float -> bool
val left: x: int * y: 'a -> int * 'a
val right: x: 'a * y: int -> 'a * int
val leftRight: x: int * y: int -> bool
val mapRev: xs: int list -> bool
val mapMap: xs: int list -> bool
val sumOfLengths: xs: string list -> bool
val xs: string list
val sum: list: 'T list -> 'T (requires member (+) and member Zero)
val length: str: string -> int
val mapKeepsLength: xs: int list -> bool
val sortKeepsLength: xs: int list -> bool
val distinctTwice: xs: int list -> bool
val distinct: list: 'T list -> 'T list (requires equality)
val sortTwice: xs: int list -> bool
val idIdIsId: xs: int list -> bool
val idAppend: bool
val insertionSort: xs: 'a list -> 'a list (requires comparison)
val foldBack: folder: ('T -> 'State -> 'State) -> list: 'T list -> state: 'State -> 'State
val sortsAgree: xs: int list -> bool