test.check: let-style behaviour in 'properties/for-all'

297 Views Asked by At

Following on this question, and the blog post referenced there, is there a reason why prop/for-all does not just roll in this sort of capability directly? E.g. something like:

(require '[clojure.test.check.generators :as gen])
(require '[clojure.test.check.properties :as prop])
(require '[clojure.test.check.clojure-test :refer :all])

(defspec some-props-test
  (prop/for-all [n (gen/choose 1 10)
                 v (gen/vector gen/int n) ;; treat n like its produced value
                 e (gen/element v)]
    ... do stuff with n, v & e ...
  ))

Basically, I want to re-use the value produced by one generator in another generator and then reference both values produced within the actual test code. This would essentially extend the sugar/magic of for-all into allowing references to the generated values within the let-like block provided by the macro, as it works within the expression blocks below.

Please let me know if I'm missing something else that makes this possible or it just wouldn't make sense for some reason to implement.

2

There are 2 best solutions below

4
On BEST ANSWER

I agree that this functionality would probably be more useful than what for-all currently does. The primary reason it hasn't been changed is for backwards-compatibility (though admittedly code using the old style wouldn't break, it would just not shrink as well as it used to).

But you have more options available than just monads:

  • gen/let, which uses let-style bindings (it isn't a drop-in replacement for for-all but you can use them together)
  • com.gfredericks.test.chuck.generators/for defined in the test.chuck helper library — it's like a fancier version of gen/let
  • com.gfredericks.test.chuck.properties/for-all, in the same library, which is a drop-in replacement for for-all
0
On

I found the later blog post in that series which fully flushes out usage with test.check (needed to read up a little on monads first to grok it). So first the monad can be declared:

(require '[clojure.algo.monads :as m])

(m/defmonad gen-m
    [m-bind gen/bind
     m-result gen/return])

(def vector-and-elem
  (m/domonad gen-m
    [n (gen/choose 1 10)
     v (gen/vector gen/int n)
     e (gen/element v)]
    [n v e]))

The gen-m monad allows referencing the value that will be generated for previously declared generators.

Then, it can be used directly in the for-all call:

(defspec some-props-test
  (prop/for-all [[n v e] vector-and-elem]
    ... do stuff with n, v & e ...
  ))

You can just pass all the values that are relevant to your constraint-checking code in the expression produced by the gen-m monad call via a vector (or map if you like) and de-structure it to get what you need.

Still, it would be nice if this was done automatically in for-all, but this works well enough.