package qcheck

  1. Overview
  2. Docs
QuickCheck inspired property-based testing for OCaml

Install

Dune Dependency

Authors

Maintainers

Sources

0.8.tar.gz
md5=061005847a32c4b4252d449e26f6c8f9

Description

This module allows to check invariants (properties of some types) over randomly generated instances of the type. It provides combinators for generating instances and printing them.

Tags

test property quickcheck

Published: 03 Feb 2018

README

README.adoc

= QCheck
:toc: macro
:toclevels: 4
:source-highlighter: pygments

QuickCheck inspired property-based testing for OCaml, and combinators to
generate random values to run tests on.


The documentation can be found https://c-cube.github.io/qcheck/[here].
This library spent some time in
https://github.com/vincent-hugot/iTeML[qtest], but is now
standalone again!
Note that @gasche's
http://gasche.github.io/random-generator/doc/Generator.html[generator library]
can be useful too, for generating random values.

toc::[]

== Use

See the documentation. I also wrote
http://cedeela.fr/quickcheck-for-ocaml.html[a blog post] that explains
how to use it and some design choices; however, be warned that the API
changed in lots of small ways (in the right direction, I hope) so the code
will not work any more.
<<examples>> is an updated version of the blog post's examples.

== Build

    $ make

You can use opam:

    $ opam install qcheck

== License

The code is now released under the BSD license.

[[examples]]
== An Introduction to the Library

First, let's see a few tests. Let's open a toplevel (e.g. utop)
and type the following to load QCheck:

[source,OCaml]
----
#require "qcheck";;
----

=== List Reverse is Involutive

We write a random test for checking that `List.rev (List.rev l) = l` for
any list `l`:

[source,OCaml]
----
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`:

[source,OCaml]
----
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:

[source,OCaml]
----
# QCheck.Test.check_exn test;;
Exception:
QCheck.Test.Test_fail ("my_buggy_test", ["[0; 1] (after 23 shrink steps)"]).
----

In this case QCheck found the minimal counterexample `[0;1]` to the property
`List.rev l = l` and it spent 23 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):

----
# QCheck_runner.run_tests [test];;

--- Failure --------------------------------------------------------------------

Test my_buggy_test failed (10 shrink steps):

[0; 1]
================================================================================
failure (1 tests failed, 0 tests errored, ran 1 tests)
- : int = 1
----


For an even nicer output `QCheck_runner.run_tests` also accepts an optional
parameter `~verbose:true`.



=== Mirrors and Trees


`QCheck` provides many useful combinators to write
generators, especially for recursive types, algebraic types,
and tuples.

Let's see how to generate random trees:

[source,OCaml]
----
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:

[source,OCaml]
----
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_runner.run_tests [test_buggy];;
----

This test fails with:

[source,OCaml]
----

--- 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:

[source,OCaml]
----
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_runner.run_tests [test_mirror];;

----


=== Preconditions

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.

[source,OCaml]
----
let test_hd_tl =
  QCheck.(Test.make
    (list int) (fun l ->
      assume (l <> []);
      l = List.hd l :: List.tl l));;

QCheck_runner.run_tests [test_hd_tl];;
----

=== Long tests

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 projet 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`.

=== Runners

The module `QCheck_runner` defines several functions to run tests, including
compatibility with `OUnit`.
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.

=== Integration within OUnit

http://ounit.forge.ocamlcore.org/[OUnit] is a popular unit-testing framework
for OCaml.
QCheck provides some helpers, in `QCheck_runner`, to convert its random tests
into OUnit tests that can be part of a wider test-suite.

[source,OCaml]
----
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_runner.to_ounit_test [passing; failing])

----

Dependencies (5)

  1. ounit
  2. base-unix
  3. base-bytes
  4. jbuilder >= "1.0+beta7"
  5. ocaml >= "4.02.0"

Dev Dependencies

None

Used by (42)

  1. archsat
  2. base32
  3. batteries >= "2.7.0" & < "3.4.0"
  4. bencode >= "2.0"
  5. binsec >= "0.4.0"
  6. bt
  7. cborl
  8. clp_operations
  9. containers >= "2.8" & < "3.0"
  10. containers-data = "3.8"
  11. containers-thread = "3.8"
  12. docfd >= "2.2.0"
  13. dolmen >= "0.7" & < "0.8.1"
  14. eris
  15. eris-lwt
  16. ezgzip
  17. gen >= "0.5.1" & < "0.5.3"
  18. inferno >= "20220603"
  19. iter < "1.2.1"
  20. lbvs_consent >= "2.0.0"
  21. lt-code
  22. lua_pattern
  23. lwd < "0.3"
  24. nunchaku >= "0.5.1"
  25. oasis2opam = "0.6.0"
  26. oseq >= "0.3" & < "0.4.1" | >= "0.5.1"
  27. ppx_regexp >= "0.4.0"
  28. ppx_tyre
  29. pratter = "1.2.1" | >= "3.0.0"
  30. prbnmcn-cgrph
  31. qcstm
  32. qtest >= "2.5" & < "2.11.1"
  33. regenerate
  34. sequence >= "1.0"
  35. serde >= "0.0.2"
  36. spelll = "0.3"
  37. stdint >= "0.7.1"
  38. stramon-lib
  39. syslog-message >= "1.0.0"
  40. timedesc
  41. timere
  42. zar >= "0.9.3"

Conflicts (2)

  1. qcheck-core
  2. ounit < "2.0"