Quickcheck
This manual provides documentation for a Racket implementation of Quickcheck, a library that tests the specification of a program with randomly generated test inputs.
The original Quickcheck is a library for Haskell.
(require quickcheck) | package: quickcheck |
1 Quickstart
1.1 Installation
If you have not installed the package yet, run the following command from the command-line:
raco pkg install quickcheck
Note that this package will require Racket v6.0 or newer to install.
Alternatively, you may use the GUI package manager from DrRacket to install the package.
1.2 Running tests
To use Quickcheck, you first write a specification for a program as a property in ordinary Racket code. For example, the following is a property that expresses the English specification “the string->number function produces numbers when given strings”:
> (define string->number-returns-number (property ([str arbitrary-string]) (number? (string->number str))))
Given such a property, we can run random tests using quickcheck:
> (quickcheck string->number-returns-number)
Falsifiable, after 0 tests:
str = "\u0002\u0003\u0002"
You may have already guessed that this property will be easily falsified, since most strings do not actually parse as numbers.
Next, let’s write a property that we expect will actually succeed. For example, the following encodes the English specification “given two lists of integers, the result of appending them will have the same length as the sum of the original list lengths”:
> (define append-and-length-agree (property ([lst-1 (arbitrary-list arbitrary-integer)] [lst-2 (arbitrary-list arbitrary-integer)]) (= (+ (length lst-1) (length lst-2)) (length (append lst-1 lst-2)))))
Testing the property reveals that it holds up:
> (quickcheck append-and-length-agree) OK, passed 100 tests.
1.3 Creating new generators
> (struct point (x y) #:transparent)
> (define (distance p1 p2) (let ([dx (- (point-x p1) (point-x p2))] [dy (- (point-y p1) (point-x p2))]) (sqrt (+ (* dx dx) (* dy dy)))))
> (define (choose-point [min-coord -9999] [max-coord 9999]) (bind-generators ([x (choose-integer min-coord max-coord)] [y (choose-integer min-coord max-coord)]) (point x y)))
> (quickcheck (property ([p1 (choose-point)] [p2 (choose-point)]) (= (distance p1 p2) (distance p2 p1))))
Falsifiable, after 0 tests:
p1 = #(struct:point -4526 6529) p2 = #(struct:point 8436 4434)
Oops, there is a bug in the distance function. If you fix it, all the checks should pass.
2 Running checks
procedure
(quickcheck prop) → void?
prop : testable?
Prints the result of the check, including a counterexample for the property if one was found.
> (quickcheck (property ((str arbitrary-string)) (string=? str (list->string (string->list str))))) OK, passed 100 tests.
an integer representing the number of tests run,
a list of ...,
either #t if the tests were successful, #f if the arguments were exhausted, or an instance of the result structure type on a test failure.
> (define-values (ntests stamps ok?) (quickcheck-results (property ((str arbitrary-string)) (string=? str (list->string (string->list str)))))) > ntests 100
> (first stamps) '()
> ok? #t
Prints the result of the check, including a counterexample for the property if one was found.
3 Configuration, results, and utilities
struct
(struct config (max-test max-fail size print-every) #:extra-constructor-name make-config) max-test : number? max-fail : number? size : (-> integer? integer?) print-every : (-> integer? (listof any/c) any)
The max-test field represents the maximum number of succeeding tests that will be run. The max-fail field represents the maximum number of tests that can be run with no result before the checker terminates.
The size field should be a function of one argument that produces the test size given the current test number. The print-every field should be a function that takes the test number and the generated arguments and is called for its side effect.
The quickcheck and quickcheck-results functions use a default config where the test count is 100, the test size for test n is (+ 3 (quotient n 2)), the max fail count is ten times the test count, and nothing is printed. The default config can be adjusted to run different numbers of tests with with-small-test-count, with-medium-test-count, and with-large-test-count.
syntax
(with-small-test-count body ...)
> (with-small-test-count (quickcheck (property ((str arbitrary-string)) (string=? str (list->string (string->list str)))))) OK, passed 100 tests.
syntax
(with-medium-test-count body ...)
> (with-medium-test-count (quickcheck (property ((str arbitrary-string)) (string=? str (list->string (string->list str)))))) OK, passed 1000 tests.
syntax
(with-large-test-count body ...)
> (with-large-test-count (quickcheck (property ((str arbitrary-string)) (string=? str (list->string (string->list str)))))) OK, passed 10000 tests.
syntax
(with-test-count test-count-expr body ...)
> (with-test-count 42 (quickcheck (property ((str arbitrary-string)) (string=? str (list->string (string->list str)))))) OK, passed 42 tests.
struct
(struct result (ok stamp arguments-list) #:extra-constructor-name make-result) ok : (or/c null #t #f) stamp : (listof string?) arguments-list : (listof any/c)
The stamp field represents the labels that were relevant to this test execution. The arguments-list is a list of the values generated for checking this test case.
syntax
(property ([id gen/arb-expr] ...) body0 body ...)
gen/arb-expr : (or/c arbitrary? generator?)
The ids are bound to the result of the given gen/arb-exprs inside the body expressions. The body expressions are used as the bodies of a predicate function that will be run with newly generated values from the specified generators or arbitraries.
> (property ((str arbitrary-string)) (string=? str (list->string (string->list str)))) #<property>
Values that can be tested are the following:
boolean values (#t and #f),
instances of the result structure type,
instances of the property structure type,
or instances of the generator structure type.
4 Integration with RackUnit
By default quickcheck simply displays whether the property check results in success or, in case of failure, it prints the arguments that falsify the property along with some other metadata. This is not helpful for (semi-) automatic testing as it is done using RackUnit. Fortunately it is possible to combine the best of both worlds.
(require rackunit/quickcheck) | package: quickcheck |
syntax
(check-property prop)
For example, let us check the previous string->number-returns-number property:
> (check-property string->number-returns-number)
--------------------
FAILURE
name: check-property
location: eval:20:0
params: '(#<property>)
ntest: 0
stamps: ()
arguments: "str = \"\\u0002\\u0003\\u0002\""
Falsifiable
--------------------
syntax
(check-property/config config prop)
syntax
(add-property-check-info ((name value) ...))
The purpose of this form is to improve error messages when a property fails. For instance, to compare whether two functions are observationally equivalent for arbitrary arguments, we can define the following form using add-property-check-info to state the expected and actual values inside the declaration of a property:
(define-syntax-rule (conforms-to f g ([name gen] ...)) (property ([name gen] ...) (let ([expected (f name ...)] [actual (g name ...)]) (add-property-check-info (['expected expected] ['actual actual])) (equal? expected actual)))) (define (f n) (+ n 1)) (define (g n) (- n 1))
> (check-property (conforms-to f g ([n arbitrary-integer])))
--------------------
FAILURE
name: check-property
location: eval:24:0
expected: 1
actual: -1
ntest: 0
stamps: ()
arguments: "n = 0"
Falsifiable
--------------------
Crucially, every time the property is tested with a different set of arguments add-property-check-info overrides the previous data.
5 Generators
struct
(struct generator (proc) #:extra-constructor-name make-generator) proc : (-> integer? random-generator? any/c)
The proc value should be a function that accepts an integer representing the size of the value, a random number generator, and returns a value for testing.
procedure
(choose-integer lower upper) → generator?
lower : integer? upper : integer?
procedure
(choose-real lower upper) → generator?
lower : real? upper : real?
value
value
procedure
(choose-char lower upper) → generator?
lower : char? upper : char?
procedure
(choose-list elem-gen size) → generator?
elem-gen : generator? size : integer?
procedure
(choose-vector elem-gen size) → generator?
elem-gen : generator? size : integer?
procedure
(choose-string char-gen size) → generator?
char-gen : generator? size : integer?
procedure
(choose-symbol char-gen size) → generator?
char-gen : generator? size : integer?
procedure
(choose-one-of opts) → generator?
opts : (listof any/c)
procedure
(choose-mixed promises) → generator?
promises : (listof (promise/c generator?))
procedure
(choose-with-frequencies freqs) → generator?
freqs : (listof (cons/c integer? generator?))
procedure
(generator-unit val) → generator?
val : any/c
syntax
(bind-generators ([id val-expr] ...) body)
If val-expr produces a generator?, the remaining code is threaded through generator-bind, and id will be bound to the value produced by the generator. Note that this delays evaluation until the generator is asked to produce a value.
(struct foo (a b) #:transparent) ; choose-foo creates a generator that produces a foo such that ; foo-a is an integer? ; foo-b is (or/c foo? #f) (define (choose-foo [recurse-limit 3]) (bind-generators ([rand (choose-integer 0 1)] [recurse? (and (positive? recurse-limit) (= 0 rand))] [a (choose-integer 0 9)] [b (if recurse? (choose-foo (sub1 recurse-limit)) #f)]) (foo a b)))
procedure
(generator-bind gen k) → generator?
gen : generator? k : (-> any/c generator?)
procedure
(generator-sequence gens) → generator?
gens : (listof generator?)
procedure
(sized f) → generator?
f : (-> integer? generator?)
value
procedure
(arbitrary gen trans) → arbitrary?
gen : generator? trans : (-> any/c generator? generator?)
value
value
value
value
value
value
value
procedure
(arbitrary-mixed pred+promises) → arbitrary?
pred+promises : (listof (cons/c (-> any/c any/c) (promise/c arbitrary?)))
procedure
(arbitrary-one-of eql? vals) → arbitrary?
eql? : (any/c any/c -> any/c) vals : (listof any/c)
procedure
(arbitrary-pair fst rst) → arbitrary?
fst : arbitrary? rst : arbitrary?
procedure
(arbitrary-list elem) → arbitrary?
elem : arbitrary?
procedure
(arbitrary-vector elem) → arbitrary?
elem : arbitrary?
procedure
(arbitrary-tuple elem ...) → arbitrary?
elem : arbitrary?
procedure
(arbitrary-record constructor accessors elem ...) → arbitrary? constructor : procedure? accessors : (listof procedure?) elem : arbitrary?
value
value
value
procedure
(arbitrary-procedure result arg ...) → arbitrary?
result : arbitrary? arg : arbitrary?
6 Operations on properties
syntax
(==> bool-expr prop)
If bool-expr is #t, equivalent to prop. Otherwise, produces a property that returns no result.
procedure
(label str test) → generator?
str : string? test : testable?
syntax
(classify really? label-expr testable-expr)
syntax
(trivial really? testable-expr)
procedure
(collect lbl test) → generator?
lbl : any/c test : testable?
7 Random number generation
procedure
(random-generator-next rand) →
integer? random-generator? rand : random-generator?
procedure
→
random-generator? random-generator? rand : random-generator?
procedure
(random-integer rg low high) → integer?
rg : random-generator? low : integer? high : integer?