Property-based tests are often contrasted with "example-based tests",
which are tests which test a function by enumerating specific inputs
and the expected outputs (i.e., "examples"). This guide is written in
terms of testing pure functions, but for testing less pure systems you
can imagine a function that wraps the test, which uses the arguments
to set up the context for the system, runs the system, and then
queries the environment to measure the effects, and returns the result
of those queries.
Property-based testing, in contrast, describes properties that should
be true for all valid inputs. A property-based test consists of a
method for generating valid inputs (a "generator"), and a function
which takes a generated input and combines it with the function under
test to decide whether the property holds for that particular input.
A classic first example of a property is one that tests the sort
function by checking that it’s idempotent. In test.check, this could
be written like this:
(require '[clojure.test.check :as tc])
(require '[clojure.test.check.generators :as gen])
(require '[clojure.test.check.properties :as prop])
(def sort-idempotent-prop
(prop/for-all [v (gen/vector gen/int)]
(= (sort v) (sort (sort v)))))
(tc/quick-check 100 sort-idempotent-prop)
;; => {:result true,
;; => :pass? true,
;; => :num-tests 100,
;; => :time-elapsed-ms 28,
;; => :seed 1528580707376}
Here the (gen/vector gen/int)
expression is the generator for inputs
to the sort
function; it specifies that an input is a vector of
integers. In reality, sort
can take any collection of compatibly
Comparable
objects; there’s often a tradeoff between the simplicity
of a generator and the completeness with which it describes the actual
input space.
The name v
is bound to a particular generated vector of integers,
and the expression in the body of the prop/for-all
determines
whether the trial passes or fails.
The tc/quick-check
call "runs the property" 100 times, meaning it
generates one hundred vectors of integers and evaluates
(= (sort v) (sort (sort v)))
for each of them; it reports success
only if each of those trials passes.
If any of the trials fails, then test.check attempts to "shrink" the
input to a minimal failing example, and then reports the original
failing example and the shrunk one. For example, this faulty property
claims that after sorting a vector of integers, the first element
should be less than the last element:
(def prop-sorted-first-less-than-last
(prop/for-all [v (gen/not-empty (gen/vector gen/int))]
(let [s (sort v)]
(< (first s) (last s)))))
If we run this property with tc/quick-check
, it returns something
like this:
{:num-tests 5,
:seed 1528580863556,
:fail [[-3]],
:failed-after-ms 1,
:result false,
:result-data nil,
:failing-size 4,
:pass? false,
:shrunk
{:total-nodes-visited 5,
:depth 2,
:pass? false,
:result false,
:result-data nil,
:time-shrinking-ms 1,
:smallest [[0]]}}
The original failing example [-3]
(given at the :fail
key) has
been shrunk to [0]
(under [:shrunk :smallest]
), and a variety of
other data is provided as well.