The idea
test.check is a Clojure
property-based testing tool inspired by QuickCheck from Haskell
.
The core idea of test.check
is that instead of enumerating expected input and output for unit tests, you write properties about your function that should
hold true for all inputs.
This lets you write concise, powerful tests.
In a sense it gives you the illusion that you deal with the infinity.
Code examples
First, let’s require test.check
:
(ns my.test
(:require [clojure.test.check :as tc]
[clojure.test.check.generators :as gen]
[clojure.test.check.properties :as prop :include-macros true]))
Let’s say we’re testing a sort function. We want to check that that our sort function is idempotent - that is, applying sort twice should be equivalent to
applying it once: (= (sort a) (sort (sort a)))
.
Let’s write a quick test to make sure this is the case:
(def sort-idempotent-prop
(prop/for-all [v (gen/vector gen/int)]
(= (sort v) (sort (sort v)))))
(tc/quick-check 100 sort-idempotent-prop)
In prose, this test reads: for all vectors of integers, v
, sorting v
is equal to sorting v
twice.
What happens if our test fails? test.check
will try and find ‘smaller’ inputs that still fail. This process is called shrinking. Let’s see it in action:
(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)))))
(tc/quick-check 100 prop-sorted-first-less-than-last)
This test claims that the first element of a sorted vector should be less-than the last. Of course, this isn’t true: the test fails with input [3]
, which gets shrunk down to [0]
, as seen in the output above.
Deterministic Randomness
Each time you call tc/quick-check
, test.check
generates different test cases - as you can see in this two examples where the failing test cases are always different:
(:fail (tc/quick-check 100 prop-sorted-first-less-than-last))
(:fail (tc/quick-check 100 prop-sorted-first-less-than-last))
But what if you want to re-run exactly the same values?
No problem: pass :seed
to tc/quick-check
and you’ll run always the same values:
(:fail (tc/quick-check 100 prop-sorted-first-less-than-last :seed 1477508406394))
(:fail (tc/quick-check 100 prop-sorted-first-less-than-last :seed 1477508406394))
Shrinking
As your test functions require more sophisticated input, shrinking becomes critical to being able to understand exactly why a random test failed.
To see how powerful shrinking is, let’s come up with a contrived example: a function that fails if it’s passed a sequence that contains the number 12
:
(def prop-no-12
(prop/for-all [v (gen/vector gen/int)]
(not (some #{12} v))))
(tc/quick-check 100 prop-no-12)
We see that the test failed on a rather large vector, as seen in the :fail
key. But then test.check
was able to shrink the input down to [12]
, as seen in the keys [:shrunk :smallest]
.
zipmap
(zipmap keys vals)
allows you to creates a map with the keys
mapped to the corresponding vals
.
(keys map)
retrieves the keys of a map.
(vals map)
retrieves the values of a map.
But how well do they combine together?
According to keys
and vals
docstrings, the keys and the values are returned in the same order - the order of (seq map)
And indeed, for map with 100 pairs of random integers, zipmap
, keys
and vals
are consistent:
(def n 100)
(def mm (zipmap (shuffle (range n)) (shuffle (range n))))
[(count mm) (= mm (zipmap (keys mm) (vals mm)))]
You can try to increase the value of n
- and it will remain true. But is it a proof? What about keys and values from other types?
Let’s check it for sure - using a more advanced random genertor, provided by test.check
:
(def random-map (gen/map (gen/one-of [gen/keyword gen/string gen/boolean gen/int gen/symbol]) gen/int))
(gen/one-of generators)
creates a generator that randomly chooses a value from the list of
generators
.
(gen/map map key-gen val-gen)
creates a generator that generates maps, with keys chosen from key-gen
and values chosen from val-gen
.
Let’s look at some samples - with gen/sample
:
(gen/sample random-map)
Now, we can test the consistency of zipmap
, keys
and vals
:
(def zipmap-keys-vals-consistency
(prop/for-all [m (gen/map (gen/one-of [gen/keyword gen/string gen/boolean gen/int gen/symbol]) gen/int)]
(= m (zipmap (keys m) (vals m)))))
(tc/quick-check 15 zipmap-keys-vals-consistency)
It seems that the docstrings were right: zipmap
, keys
and vals
are consistent.
Check test.check documentation for additional functions and explanations.
Conclusion
I hope that you enjoyed this interactive tutorial about generative testing in clojure
. This is really a powerful paradigm that might change forever the way you write and think about testing. And who know? Maybe it will catch a bug or two…
Please forward it to your friends if you liked it and share your critics on twitter @viebel or on slack #klipse
channel.