Howto write test.check custom generators

609 views
Skip to first unread message

Timothy Washington

unread,
Aug 10, 2014, 2:39:24 PM8/10/14
to clo...@googlegroups.com
Hi there, 

I'm trying to get my head wrapped around test.check. My current stumbling block is custom generators. Of course I've combed through the docs and source. And I'm pretty sure I need to use gen-bind to get this all working. But it's just not clicking yet. 


Let's say I want to create a user and a group, where a user can belong to many groups. So let's say I have a generic create-user function. 

  (defn create-user [opts]
    (merge {:id (mu/generate-uuid)
            :username ""
            :password (crypto/base64 12)
            :first-name ""
            :last-name ""
            :email ""
            :country {}}
           opts))

And a generic create-group function. 

  (defn create [opts]
    (merge {:id (mu/generate-uuid)
            :name ""
            :users []}
           opts))


  1. Now, if I try `(gen/sample create-user)`, or `(gen/sample (gen/vector du/create))`it will fail. 
  2. I also want to do the same thing with groups (ie (gen/sample create-user)). 
  3. Additionally however, I want to assert that a created group will always have at least 1 user in the `:users []` k/v field. 

How can I make test.check generators for these data structures? 



Tim Washington 


Sam Ritchie

unread,
Aug 10, 2014, 3:02:27 PM8/10/14
to clo...@googlegroups.com
You can to use gen/fmap to build up generators. Here's an example:

;; generates a millisecond using gen/choose, then maps that to a time instance using my u/ms-to-time function.
(def result-gen
  (->> (gen/choose 0 u/max-time)
       (gen/fmap u/ms-to-time)))

;; Generates EITHER a result or nil, then maps that into an event entry data structure:
(def event-entry-gen
  (->> (gen/one-of [(gen/return nil) result-gen])
       (gen/fmap (fn [result]
                   (merge {:_id "id"
                           :_rev "rev"
                           :type "event-entry"
                           :division {:age-group "U17"
                                      :boat-type "K1"
                                      :gender "male"}
                           :event-id "event-id"
                           :regatta-id "regatta-id"
                           :athletes []
                           :racer-number 10}
                          (when result
                            {:result {:division-size 10
                                      :rank {:overall 1
                                             :division 1}
                                      :time result}}))))))

Once you have a generator instance you can copy the rest of the test.check examples, run gen/sample on the generator directly... all the good stuff.

August 10, 2014 at 12:38 PM
--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clo...@googlegroups.com
Note that posts from new members are moderated - please be patient with your first post.
To unsubscribe from this group, send email to
clojure+u...@googlegroups.com
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en
---
You received this message because you are subscribed to the Google Groups "Clojure" group.
To unsubscribe from this group and stop receiving emails from it, send an email to clojure+u...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

--
Sam Ritchie (@sritchie)

Timothy Washington

unread,
Aug 10, 2014, 7:04:21 PM8/10/14
to clo...@googlegroups.com
Oook, this is starting to sink in. 

I've gotten a little farther, in that I can run quick-check with my custom generator. However, after a bunch of values had been generated, it fails on a NullPointerException. How can I know where the breakdown is happening? 

=> (tc/quick-check 100 u/has-agroup)
{:email , :last-name , :first-name , :username }
{:email , :last-name , :first-name , :username 0}
{:email , :last-name , :first-name Rq, :username 0}
{:email 5D2, :last-name R4F, :first-name x, :username G0H}
...
{:email 8KQgSr497EPw14X80oEbWC0, :last-name cdkLb8D5ol, :first-name 8DCOXz1U4S3JM641u5I7yPwQ, :username DADOk}
{:email x, :last-name Qq3h07d6Cbs4, :first-name 6H6Enrwu, :username yf40SClLjJHp1ptbnx4I9xFbwI3}
NullPointerException   clojure.test.check.generators/gen-bind/fn--18990 (generators.clj:77)
bkell.bkell> (pst *e)
NullPointerException
        clojure.test.check.generators/gen-bind/fn--18990 (generators.clj:77)
        clojure.test.check.generators/gen-bind/fn--18990 (generators.clj:79)
        clojure.test.check.generators/gen-bind/fn--18990 (generators.clj:77)
        clojure.test.check.generators/gen-fmap/fn--18985 (generators.clj:70)
        clojure.test.check.generators/call-gen (generators.clj:56)
        clojure.test.check/quick-check (check.clj:57)
        bkell.bkell/eval19267 (form-init7280611083923741467.clj:1)
        clojure.lang.Compiler.eval (Compiler.java:6703)
        clojure.lang.Compiler.eval (Compiler.java:6666)
        clojure.core/eval (core.clj:2927)
        clojure.main/repl/read-eval-print--6625/fn--6628 (main.clj:239)
        clojure.main/repl/read-eval-print--6625 (main.clj:239)

This is my domain function: 

  (defn create-user 
    ([] (create {}))
    ([opts]
       (println opts)
       (let [sans-nils (apply merge (for [[k v]
                                          opts
                                          :when (not (nil? v))]
                                      {k v}))]

         (merge {:id (mu/generate-uuid)
                 :username ""
                 :password (crypto/base64 12)
                 :first-name ""
                 :last-name ""
                 :email ""
                 :country {}}
                sans-nils))))

And these are my test functions. 

  (def gend-user
    (gen/fmap du/create-user
              (gen/hash-map :username gen/string-alpha-numeric
                            :first-name gen/string-alpha-numeric
                            :last-name gen/string-alpha-numeric
                            :email gen/string-alpha-numeric)))

  (def has-agroup
    (prop/for-all [v (gen/sample gend-user)]
                  (-> v nil? not)))


Tim Washington 

Reid Draper

unread,
Aug 19, 2014, 11:12:31 AM8/19/14
to clo...@googlegroups.com
Hey Tim,

When you write a property, like your `has-agroup`, there's no need to call `gen/sample`. You can simply write: (prop/for-all [v my-generator] ...)

-Reid
Reply all
Reply to author
Forward
0 new messages