QuickCheck inspired property-based testing for OCaml.
QCheck
consists of a collection of opam
packages and extensions:
qcheck-core
- provides the core property-based testing API and depends only on unix
and dune
.
qcheck-ounit
- provides an integration layer for OUnit
qcheck-alcotest
- provides an integration layer for alcotest
qcheck
- provides a compatibility API with older versions of qcheck
, using both qcheck-core
and qcheck-ounit
.
ppx_deriving_qcheck
- provides a preprocessor to automatically derive generators
In addition, the multicoretests
repository offers
qcheck-stm
- for running sequential and parallel model-based tests
qcheck-lin
- for testing an API for sequential consistency
qcheck-multicoretests-util
- a small library of utility extensions, such as properties with time outs
To construct advanced random generators, the following libraries might also be of interest:
feat
- a library for functional enumeration and sampling of algebraic data types
random-generator
- a library experimenting with APIs for random generation
Earlier qcheck
spent some time in qtest, but was since made standalone again.
You can install QCheck via opam
:
$ opam install qcheck-core
This provides a minimal installation without needless dependencies.
Install the bigger qcheck
package instead for compatibility with qcheck.0.8 and before:
To build the library from source
Normally, for contributors, opam pin https://github.com/c-cube/qcheck
will pin the 5 opam packages from this repository.
The code is now released under the BSD license.
An Introduction to the LibraryFirst, let’s see a few tests. Let’s open a toplevel (e.g. utop) and type the following to load QCheck:
Note
alternatively, it is now possible to locally do:dune utop src
to load qcheck
. List Reverse is Involutive
We write a random test for checking that List.rev (List.rev l) = l
for any list l
:
let test = QCheck.Test.make ~count:1000 ~name:"list_rev_is_involutive" QCheck.(list small_nat) (fun l -> List.rev (List.rev l) = l);; (* we can check right now the property... *) QCheck.Test.check_exn test;;
In the above example, we applied the combinator list
to the random generator small_nat
(ints between 0 and 100), to create a new generator of lists of random integers. These builtin generators come with printers and shrinkers which are handy for outputting and minimizing a counterexample when a test fails.
Consider the buggy property List.rev l = l
:
let test = QCheck.Test.make ~count:1000 ~name:"my_buggy_test" QCheck.(list small_nat) (fun l -> List.rev l = l);;
When we run this test we are presented with a counterexample:
# QCheck.Test.check_exn test;; Exception: test `my_buggy_test` failed on ≥ 1 cases: [0; 1] (after 11 shrink steps)
In this case QCheck found the minimal counterexample [0;1]
to the property List.rev l = l
and it spent 11 steps shrinking it.
Now, let’s run the buggy test with a decent runner that will print the results nicely (the exact output will change at each run, because of the random seed):
# #require "qcheck-core.runner";; # QCheck_base_runner.run_tests [test];; random seed: 452768242 --- Failure -------------------------------------------------------------------- Test my_buggy_test failed (14 shrink steps): [0; 1] ================================================================================ failure (1 tests failed, 0 tests errored, ran 1 tests) - : int = 1
For an even nicer output QCheck_base_runner.run_tests
also accepts an optional parameter ~verbose:true
.
QCheck
provides many useful combinators to write generators, especially for recursive types, algebraic types, and tuples.
Let’s see how to generate random trees:
type tree = Leaf of int | Node of tree * tree let leaf x = Leaf x let node x y = Node (x,y) let tree_gen = QCheck.Gen.(sized @@ fix (fun self n -> match n with | 0 -> map leaf nat | n -> frequency [1, map leaf nat; 2, map2 node (self (n/2)) (self (n/2))] ));; (* generate a few trees, just to check what they look like: *) QCheck.Gen.generate ~n:20 tree_gen;; let arbitrary_tree = let open QCheck.Iter in let rec print_tree = function | Leaf i -> "Leaf " ^ (string_of_int i) | Node (a,b) -> "Node (" ^ (print_tree a) ^ "," ^ (print_tree b) ^ ")" in let rec shrink_tree = function | Leaf i -> QCheck.Shrink.int i >|= leaf | Node (a,b) -> of_list [a;b] <+> (shrink_tree a >|= fun a' -> node a' b) <+> (shrink_tree b >|= fun b' -> node a b') in QCheck.make tree_gen ~print:print_tree ~shrink:shrink_tree;;
Here we write a generator of random trees, tree_gen
, using the fix
combinator. fix
is sized (it is a function from int
to a random generator; in particular for size 0 it returns only leaves). The sized
combinator first generates a random size, and then applies its argument to this size.
Other combinators include monadic abstraction, lifting functions, generation of lists, arrays, and a choice function.
Then, we define arbitrary_tree
, a tree QCheck.arbitrary
value, which contains everything needed for testing on trees:
a random generator (mandatory), weighted with frequency
to increase the chance of generating deep trees
a printer (optional), very useful for printing counterexamples
a shrinker (optional), very useful for trying to reduce big counterexamples to small counterexamples that are usually more easy to understand.
The above shrinker strategy is to
reduce the integer leaves, and
substitute an internal Node
with either of its subtrees or by splicing in a recursively shrunk subtree.
A range of combinators in QCheck.Shrink
and QCheck.Iter
are available for building shrinking functions.
We can write a failing test using this generator to see the printer and shrinker in action:
let rec mirror_tree (t:tree) : tree = match t with | Leaf _ -> t | Node (a,b) -> node (mirror_tree b) (mirror_tree a);; let test_buggy = QCheck.Test.make ~name:"buggy_mirror" ~count:200 arbitrary_tree (fun t -> t = mirror_tree t);; QCheck_base_runner.run_tests [test_buggy];;
This test fails with:
--- Failure -------------------------------------------------------------------- Test mirror_buggy failed (6 shrink steps): Node (Leaf 0,Leaf 1) ================================================================================ failure (1 tests failed, 0 tests errored, ran 1 tests) - : int = 1
With the (new found) understanding that mirroring a tree changes its structure, we can formulate another property that involves sequentializing its elements in a traversal:
let tree_infix (t:tree): int list = let rec aux acc t = match t with | Leaf i -> i :: acc | Node (a,b) -> aux (aux acc b) a in aux [] t;; let test_mirror = QCheck.Test.make ~name:"mirror_tree" ~count:200 arbitrary_tree (fun t -> List.rev (tree_infix t) = tree_infix (mirror_tree t));; QCheck_base_runner.run_tests [test_mirror];;Integrated shrinking with
QCheck2
You may have noticed the shrink_tree
function above to reduce tree counterexamples. With the newer QCheck2
module, this is not needed as shrinking is built into its generators.
For example, we can rewrite the above tree generator to QCheck2
by just changing the QCheck
occurrences to QCheck2
:
type tree = Leaf of int | Node of tree * tree let leaf x = Leaf x let node x y = Node (x,y) let tree_gen = QCheck2.Gen.(sized @@ fix (fun self n -> match n with | 0 -> map leaf nat | n -> frequency [1, map leaf nat; 2, map2 node (self (n/2)) (self (n/2))] ));; (* generate a few trees with QCheck2, just to check what they look like: *) QCheck2.Gen.generate ~n:20 tree_gen;;
QCheck2.Test.make
has a slightly different API than QCheck.Test.make
, in that it accepts an optional ~print
argument and consumes generators directly built with QCheck2.Gen
:
let rec print_tree = function | Leaf i -> "Leaf " ^ (string_of_int i) | Node (a,b) -> "Node (" ^ (print_tree a) ^ "," ^ (print_tree b) ^ ")";; let rec mirror_tree (t:tree) : tree = match t with | Leaf _ -> t | Node (a,b) -> node (mirror_tree b) (mirror_tree a);; let test_buggy = QCheck2.Test.make ~name:"buggy_mirror" ~count:200 ~print:print_tree tree_gen (fun t -> t = mirror_tree t);; QCheck_base_runner.run_tests [test_buggy];;
Being newer the QCheck2
module is less battle tested than QCheck
. QCheck2
on the other hand removes the need for having to hand-write shrinkers. QCheck
tests can be ported to QCheck2
by following the migration guide. Please file an issue if you encounter problems using either of the two modules.
The functions QCheck.assume
and QCheck.(=⇒)
can be used for tests with preconditions. For instance, List.hd l :: List.tl l = l
only holds for non-empty lists. Without the precondition, the property is false and will even raise an exception in some cases.
let test_hd_tl = QCheck.(Test.make (list int) (fun l -> assume (l <> []); l = List.hd l :: List.tl l));; QCheck_base_runner.run_tests [test_hd_tl];;
By including a precondition QCheck will only run a property on input satisfying `assume’s condition, potentially generating extra test inputs.
It is often useful to have two version of a testsuite: a short one that runs reasonably fast (so that it is effectively run each time a project is built), and a long one that might be more exhaustive (but whose running time makes it impossible to run at each build). To that end, each test has a 'long' version. In the long version of a test, the number of tests to run is multiplied by the ~long_factor
argument of QCheck.Test.make
.
The module QCheck_base_runner
defines several functions to run tests. The easiest one is probably run_tests
, but if you write your tests in a separate executable you can also use run_tests_main
which parses command line arguments and exits with 0
in case of success, or an error number otherwise.
The module QCheck_runner
from the qcheck
opam package is similar, and includes compatibility with OUnit
.
OUnit is a popular unit-testing framework for OCaml. QCheck provides a sub-library qcheck-ounit
with some helpers, in QCheck_ounit
, to convert its random tests into OUnit tests that can be part of a wider test-suite.
let passing = QCheck.Test.make ~count:1000 ~name:"list_rev_is_involutive" QCheck.(list small_nat) (fun l -> List.rev (List.rev l) = l);; let failing = QCheck.Test.make ~count:10 ~name:"fail_sort_id" QCheck.(list small_nat) (fun l -> l = List.sort compare l);; let _ = let open OUnit in run_test_tt_main ("tests" >::: List.map QCheck_ounit.to_ounit_test [passing; failing])Integration within alcotest
Alcotest is a simple and colorful test framework for OCaml. QCheck now provides a sub-library qcheck-alcotest
to easily integrate into an alcotest test suite:
let passing = QCheck.Test.make ~count:1000 ~name:"list_rev_is_involutive" QCheck.(list small_int) (fun l -> List.rev (List.rev l) = l);; let failing = QCheck.Test.make ~count:10 ~name:"fail_sort_id" QCheck.(list small_int) (fun l -> l = List.sort compare l);; let () = let suite = List.map QCheck_alcotest.to_alcotest [ passing; failing] in Alcotest.run "my test" [ "suite", suite ]
Rely is a Jest-inspire native reason testing framework. @reason-native/qcheck-rely is available via NPM and provides matchers for the easy use of qCheck within Rely.
open TestFramework; open QCheckRely; let {describe} = extendDescribe(QCheckRely.Matchers.matchers); describe("qcheck-rely", ({test}) => { test("passing test", ({expect}) => { let passing = QCheck.Test.make( ~count=1000, ~name="list_rev_is_involutive", QCheck.(list(small_int)), l => List.rev(List.rev(l)) == l ); expect.ext.qCheckTest(passing); (); }); test("failing test", ({expect}) => { let failing = QCheck.Test.make( ~count=10, ~name="fail_sort_id", QCheck.(list(small_int)), l => l == List.sort(compare, l) ); expect.ext.qCheckTest(failing); (); }); });
The ppx_deriving_qcheck
opam package provides a ppx_deriver to derive QCheck generators from a type declaration:
type tree = Leaf of int | Node of tree * tree [@@deriving qcheck]
See the according README for more information and examples.
We can use the buggy test from above using the qcheck-core
opam package:
(* test.ml *) let test = QCheck.Test.make ~count:1000 ~name:"my_buggy_test" QCheck.(list small_nat) (fun l -> List.rev l = l) let _ = QCheck_base_runner.run_tests_main [test]
with the following dune
file (note the qcheck-core.runner
sub-package):
(test (name test) (modules test) (libraries qcheck-core qcheck-core.runner) )
and run it with dune exec ./test.exe
or dune runtest
.
We recommend using the qcheck-core
package as it has a minimal set of dependencies and also avoids problems with using (implicit_transitive_deps false)
in dune.
To instead use the qcheck
opam package and its included QCheck_runner
:
(* test.ml *) let test = QCheck.Test.make ~count:1000 ~name:"my_buggy_test" QCheck.(list small_nat) (fun l -> List.rev l = l) let _ = QCheck_runner.run_tests_main [test]
with the following dune
file:
(test (name test) (modules test) (libraries qcheck) )
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4