Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I compose clojure.test.check generators in a one to many fashion?

Let us say I have a generator users-gen, that generates a group of 1 or more users. And another parameterized generator called user-actions-gen that takes a sequence of one or more users and generates a sequence of actions that those users might perform.

(def user-gen 
 ;; generates a user
 ...)

(def users-gen 
 ;; sequences of 1 or more users 
 (gen/such-that not-empty (gen/vector gen/users))

(defn user-actions-gen [users]
  ;; a action performed by some user from the `users argument
  ...)

If I want to generate a single action for a single sequence of users generated by users-gen, then it is simple, just gen/bind users-gen to user-actions-gen directly.

However, I want to generate many actions from the same sequence of users. I have this problem because I am basically just trying to say "Here is the state, let any random action come in, let us apply the action to the state, let us confirm that the state is still valid; do this for all actions." I have the following code.

(defspec check-that-state-is-always-valid
  100
  (let [state-atm (atom {})]
    (prop/for-all
     [[actions users]
      (gen/bind users-gen
                (fn [users]
                  (gen/tuple
                   (gen/vector (user-actions-gen users))
                   (gen/return users))))]
     (doseq [action actions
             :let [state (swap! state-atm state-atm-transform-fx action)]]
       (is (state-still-valid? state))))))

This sort of works. The problems are that:

  1. It seems to fully evaluate the doseq rather than halting on the first error
  2. It just looks kind of wrong. The code is all over the place, it is not entirely evident what it does.
  3. It seems like maybe user-actions-gen should be taking a generator of users-gen, rather than the realized users value of users-gen? Would this help with composability? Note that I don't want to put them together as users-gen is probably useful to other generators.

So, to recap. I am taking a single generated value from one generator and passing it as an argument to more than one generators. How do I go about doing this in a more attractive/elegant way?

like image 418
Stephen Cagle Avatar asked Oct 21 '22 08:10

Stephen Cagle


2 Answers

You might want to check out the newest version of test.check (0.9.0). It now includes a let in the generators namespace which makes composing generators a snap:

https://github.com/clojure/test.check/blob/master/src/main/clojure/clojure/test/check/generators.cljc#L1452

The downside to this is you still can't just do this directly in prop/for-all (apparently due to backwards compatibility reasons).

The alternative and what I would recommend is using test.chuck (it's written by the current maintainer of test.check anyway). It does have a for-all whose binding-forms work just like generators/let does. This is the cleanest approach i've found and works quite nicely.

like image 68
leeor Avatar answered Oct 23 '22 03:10

leeor


I would make two main changes to what you're currently doing:

  1. Pull out your inline gen/bind into a new generator, tentatively named users-and-actions-gen (also note that I swapped the order of users and actions in the result to match the name):

    (def users-and-actions-gen
      (gen/bind users-gen
                (fn [users]
                  (gen/tuple
                   (gen/return users)
                   (gen/vector (user-actions-gen users))))))
    
  2. Don't use an atom for testing something which doesn't need it. You can instead generate a lazy sequence of states, and just test that they all have the property you're looking for. That way it will read nicely as well as having the short-circuit property you're looking for:

    (defspec check-that-state-is-always-valid
      100
      (prop/for-all [[users actions] users-and-actions-gen]
        (is (every? state-still-valid? 
                    (reductions (fn [state action]
                                  (state-atm-transform-fx state action))
                                {} actions)))))
    

Other than those two changes, I'm not sure you can really improve on what you're doing. I think your users-and-actions-gen is fairly specific. It can be slightly generalised, but I'm not sure the generalisation would be all that useful (it would essentially be a restricted bind). What I've proposed above should solve your issues (1) and (2), but I don't think (3) is really an issue.

like image 22
mange Avatar answered Oct 23 '22 04:10

mange