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
smime.p7s
Description: S/MIME cryptographic signature