Dear Clojurians,

Stuart brought up the idea of implementing a QuickCheck clone
for Clojure. I think that this is a good idea, since it provides
a much better coverage of possible inputs. I gave it some thought
this weekend and the following is, what I came up with. There
are some specifics of the API which I will ignore in the beginning.
They will be explained later on.

Comments, thoughts, improvements appreciated.

ClojureCheck generates random input samples and feeds them to
the functions under test. The generation is done via generation
functions, which are combined via so-called combinators to more
complex generators.

The core is the function arbitrary. It generates the random values
for a given type. As Stuart already suggested, I think a multi
method is the best way to model this.

  (defmulti arbitrary (fn [x & _] (if (vector? x) (first x) x)))

The first value is itself normally unused, since it normally
only gives the type on which we dispatch. So we can for example
define a generator for integers.

  (defmethod arbitrary Integer
    [_ size]
    (int (Math/round (* size (.nextDouble *prng*)))))

  (arbitrary Integer 100)

To use a generator one uses for-all.

  (for-all [x Integer
            y Integer]
    (test-is/is (= (+ x y) (+ y x))))

This code actually cannot be used directly. You may have noticed
the unexplained size parameter in the arbitrary call. This is a
special parameters passed to the generators to allow a scaling
of the test samples. So a generator of lists may start with short
lists and may generate longer and longer lists as the test iterates.
To hide this from the developer for-all actually returns a generator
itself, ie. a function which accepts the size parameter and passes
it on the generators in the binding form. So to run actually the
tests, one uses the property function.

  (property
    (for-all [x Integer
              y Integer]
      (test-is/is (= (+ x y) (+ y x)))))

This runs the tests for certain (configurable) number of iterations.
Binding x and y to random integers each time.

But sometimes one wants to pass parameters to the generators. I
thought about the way eg. require does this: (require [foo :as bar]).
So I thought one could do this by specifying a vector in the binding
form.

  (for-all [x [:IntegerRange 100 1000]]
    (test-something-with x))

  (defmethod arbitrary :IntegerRange
    [[_ mn mx] _]
    (int (Math/round (+ mn (* (- mx mn) (.nextDouble *prng*))))))

The environment like the random number generator, the number of
test iterations etc. would be configurable at runtime.

A first prototype looks promising. But before I run to far in the
wrong direction, I'd like to ask for comments on the API so far.
Especially things like passing arguments via a vector...

Sincerely
Meikel

Attachment: smime.p7s
Description: S/MIME cryptographic signature

Reply via email to