ClojureCheck is back.
It brings specification based testing to Clojure and integrates
(almost) seamless with clojure.test
. So what does specification
based testing mean and how does it help you?
ClojureCheck is one of my oldest Clojure projects. It started as ClojureTAP -- a testing library producing TAP output. Later on I started to port QuickCheck to Clojure. And hence renamed it. However, I never got really far and things stalled.
I feel very sad about this. It's the best documented Clojure project
I ever did and it brings up memories from times where todays require
system everyone complains about didn't even exist. So for quite some
time now, I had the urge to return ClojureCheck. I just didn't want
to let it rot further.
Before I started with the work I had to decide where to hook into.
Originally ClojureCheck was developed more or less in parallel to
clojure.contrib.test-is
(now: clojure.test
). So it basically
contains a similar machinery. However, clojure.test
is now the
de-facto standard being included in Clojure itself. So there is no
reason to do things twice and it hooking into clojure.test
is a
no-brainer. (Very sad: a lot of nice work gets nuked.)
I also did some more research. I based my original approach too much
on QuickCheck. But this gives raise to some problems. In particular
Haskell has one more dimension of information: the type system. So you
can have one function – arbitrary
– which does the right thing
depending on the return type you declare. In Clojure this type of
dispatch does not really fly.
Tom Moertel's LectroTest is written in Perl which is similar to Clojure in terms of dynamism. I took LectroTest as a source of inspiration for the port to Clojure.
So after this rather long introduction: What is ClojureCheck?
ClojureCheck provides facilities to easily generate random test data, which are then used to „prove“ certain properties of the code under test. One can eg. generate test data based on a specification. Tom Moertel does this for email addresses in his talk on LectroTest.
Let's use another example from LectroTest: angular-diff
.
(ns our.angular.diff
(:use clojure.test)
(:require [clojurecheck.core :as cc]))
(defn angular-diff
[a b]
(-> (- a b) Math/abs (mod 180)))
angular-diff
returns the smallest angle between two given angles
a
and b
. So let's write some tests.
(deftest angular-diff-test
; Identical angles should be zero
(are [a b r] (= (angular-diff a b) r)
0 0 0
90 90 0)
; Order of angles shouldn't matter
(are [a b r] (= (angular-diff a b) r)
0 45 45
45 0 45
(is (= (angular-diff 0 270) 90)
"Should return the smallest angle")
(let [a (* 360 2)
b (* 360 4)]
(is (= (angular-diff a (+ b 23)) 23)
"multiples of 360 degrees shouldn't matter"))))
And fire:
our.angular.diff=> (run-tests)
Testing our.angular.diff
Ran 1 tests containing 4 assertions.
0 failures, 0 errors.
{:type :summary, :test 1, :pass 4, :fail 0, :error 0}
Yeah! The world is good!
Is it? Let's apply ClojureCheck to our test problem. We want to generate the problems. So as Tom Moertel says in his talk: we need needles and an easy way to spot them.
We guess some arbitrary angle a
. Now just taking a second angle
b
wouldn't help as very much. So we have somehow to work our way
backwards: from the solution to the input.
So next we guess our result: diff
. When we feed angular-diff
with a
and (+ a diff)
the expected result is diff
. However
adding multiples of 360° don't change things. So we guess a
winding count n
and also add it. So whenever `(angular-diff
a (+ a (* n 360) diff))≠
(Math/abs diff)` we found our needle!
So let's see how we can cast this in code.
(deftest angular-diff-property
(cc/property "angular diff returns the smallest angle between a and b"
[a (cc/int)
n (cc/int)
diff (cc/int :lower -180 :upper 180)]
(let [b (+ a (* n 360) diff)]
(is (= (angular-diff a b) (Math/abs diff))))))
And now let's run our test:
our.angular.diff=> (run-tests)
Testing our.angular.diff
FAIL in (angular-diff-property) (core.clj:305)
falsified 'angular diff returns the smallest angle between a and b' in 3 attempts
inputs where:
a = 1
n = -1
diff = 1
failed assertions where:
expected: (= (angular-diff a b) (Math/abs diff))
actual: (not (= 179 1))
Ran 2 tests containing 5 assertions.
1 failures, 0 errors.
{:type :summary, :test 2, :pass 4, :fail 1, :error 0}
Dang! :( The world is bad.
But we shouldn't think like this. The world is not bad! We are just too stupid. We should be happy about the failing test, because it tells us, that we did a mistake in our code. And a mistake is always an occasion to learn. (Oh, dear. The QA guy shines through…)
So to fix our function we have to adapt for angles where the difference is greater than 180°.
(defn angular-diff
[a b]
(let [diff (-> (- a b) Math/abs (mod 360))]
(if (> diff 180) (- 360 diff) diff)))
Now a test run should look like this:
our.angular.diff=> (run-tests)
Testing our.angular.diff
Ran 2 tests containing 5 assertions.
0 failures, 0 errors.
{:type :summary, :test 2, :pass 5, :fail 0, :error 0}
Testing systems with random input can help. It depends on how you do it. In particular stimulating the system with random input of a pre-defined structure can help finding edge cases hidden deep in the logic.
For unit testing it can help to find blind spots in the input coverage. But with carefully designed tests there might not be any improvements at all.
So in the end: YMMV.
Published by Meikel Brandmeyer on .
I'm a long-time Clojure user and the developer of several open source projects mostly involving Clojure. I try to actively contribute to the Clojure community.
My most active projects are at the moment VimClojure, Clojuresque and ClojureCheck.
Copyright © 2009-2014 All Right Reserved. Meikel Brandmeyer