2

Assuming I have already defined a spec from which I'd like to generate test data:

(s/def :customer/id uuid?)
(s/def :customer/given-name string?)
(s/def :customer/surname string?)
(s/def :customer/age pos?)
(s/def ::customer
  (s/keys
    :req-un [:customer/id
             :customer/given-name
             :customer/surname
             :customer/age]))

In generating test data, I'd like to override how ids are generated in order to ensure they're from a smaller pool to encourage collisions:

(defn customer-generator
  [id-count]
  (gen/let [id-pool (gen/not-empty (gen/vector (s/gen :customer/id) id-count))]
    (assoc (s/gen ::customer) :id (gen/element id-pool))))

Is there a way I can simplify this by overriding the :customer/id generator in my test code and then just using (s/gen ::customer)? So, something like the following:

(with-generators [:customer/id (gen/not-empty (gen/vector (s/gen :customer/id) id-count)))]
  (s/gen ::customer))
Alan Thompson
  • 29,276
  • 6
  • 41
  • 48
Tim Clemons
  • 6,231
  • 4
  • 25
  • 23

3 Answers3

4

Officially, you can override generators for specs by passing an overrides map to s/gen (See the docstring for more details):

(s/def :customer/id uuid?)
(s/def :customer/given-name string?)
(s/def :customer/surname string?)
(s/def :customer/age nat-int?)
(s/def ::customer
  (s/keys
    :req-un [:customer/id
             :customer/given-name
             :customer/surname
             :customer/age]))

(def fixed-customer-id (java.util.UUID/randomUUID))
fixed-customer-id
;=> #uuid "c73ff5ea-8702-4066-a31d-bc4cc7015811"
(gen/generate (s/gen ::customer {:customer/id #(s/gen #{fixed-customer-id})}))
;=> {:id #uuid "c73ff5ea-8702-4066-a31d-bc4cc7015811",
;    :given-name "1042IKQhd",
;    :surname "Uw0AzJzj",
;    :age 104}

Alternatively, there is a library for such stuff named genman, which I developed before :) Using it, you can also write as:

(require '[genman.core :as genman :refer [defgenerator]])

(def fixed-customer-id (java.util.UUID/randomUUID))

(genman/with-gen-group :test
  (defgenerator :customer/id
    (s/gen #{fixed-customer-id})))

(genman/with-gen-group :test
  (gen/generate (genman/gen ::customer)))
athos
  • 113
  • 1
  • 5
  • This worked for me, with the caveat that only when my spec base types didn't directly reference another spec. For instance, given: ```clj (s/def :data/uuid uuid?) (s/def :customer/id :data/uuid) ``` Trying to override the `:customer/id` generator then doesn't work, but overriding `:data/uuid` does. The workaround I found was this: ```clj (s/def :customer/id (s/and :data/uuid)) ``` Which seems like a bit of a kluge. – Tim Clemons Oct 28 '21 at 21:21
0

Clojure spec uses test.check internally to generate sample values. Here is how test.check can be overridden. Whenever trying to write unit tests with a "fake" function, with-redefs is your friend:

(ns tst.demo.core
  (:use tupelo.core tupelo.test)
  (:require
    [clojure.test.check.generators :as gen]
    ))

(def  id-gen gen/uuid)
(dotest
  (newline)
  (spyx-pretty (take 3 (gen/sample-seq id-gen)))

  (newline)
  (with-redefs [id-gen (gen/choose 1 5)]
    (spyx-pretty (take 33 (gen/sample-seq id-gen))))
  (newline)
  )

with result:

-----------------------------------
   Clojure 1.10.3    Java 15.0.2
-----------------------------------

Testing tst.demo.core

(take 3 (gen/sample-seq id-gen)) => 
[#uuid "cbfea340-1346-429f-ba68-181e657acba5"
 #uuid "7c119cf7-0842-4dd0-a23d-f95b6a68f808"
 #uuid "ca35cb86-1385-46ad-8fc2-e05cf7a1220a"]

(take 33 (gen/sample-seq id-gen)) => 
[5 4 3 3 2 2 3 1 2 1 4 1 2 2 4 3 5 2 3 5 3 2 3 2 3 5 5 5 5 1 3 2 2]

Example created using my favorite template project.


Update

Unfortunately, the above technique does not work for Clojure Spec since (s/def ...) uses a global registery of Spec definitions, and is therefore immune to with-redefs. However, we can overcome this definition by simply redefining the desired spec in the unit test namespace like:

(ns tst.demo.core
  (:use tupelo.core tupelo.test)
  (:require
    [clojure.spec.alpha :as s]
    [clojure.spec.gen.alpha :as gen]
  ))

(s/def :app/id (s/int-in 9 99))
(s/def :app/name string?)
(s/def :app/cust (s/keys :req-un [:app/id :app/name]))

(dotest
  (newline)
  (spyx-pretty (gen/sample (s/gen :app/cust)))

  (newline)
  (s/def :app/id (s/int-in 2 5)) ; overwrite the definition of :app/id for testing
  (spyx-pretty (gen/sample (s/gen :app/cust)))

  (newline))

with result

-----------------------------------
   Clojure 1.10.3    Java 15.0.2
-----------------------------------

Testing tst.demo.core

(gen/sample (s/gen :app/cust)) => 
[{:id 10, :name ""}
 {:id 9, :name "n"}
 {:id 10, :name "fh"}
 {:id 9, :name "aI"}
 {:id 11, :name "8v5F"}
 {:id 10, :name ""}
 {:id 10, :name "7"}
 {:id 10, :name "3m6Wi"}
 {:id 13, :name "OG2Qzfqe"}
 {:id 10, :name ""}]

(gen/sample (s/gen :app/cust)) => 
[{:id 3, :name ""}
 {:id 3, :name ""}
 {:id 2, :name "5e"}
 {:id 3, :name ""}
 {:id 2, :name "y01C"}
 {:id 3, :name "l2"}
 {:id 3, :name "c"}
 {:id 3, :name "pF"}
 {:id 4, :name "0yrxyJ7l"}
 {:id 4, :name "40"}]

So, it's a little ugly, but the redefinition of :app/id does the trick, and it only takes effect during unit test runs, leaving the main application unaffected.

Alan Thompson
  • 29,276
  • 6
  • 41
  • 48
0
user> (def ^:dynamic *idgen* (s/gen uuid?))
#'user/*idgen*
user> (s/def :customer/id (s/with-gen uuid? (fn [] @#'*idgen*)))
:customer/id
user> (s/def :customer/age pos-int?)
:customer/age
user> (s/def ::customer (s/keys :req-un [:customer/id :customer/age]))
:user/customer
user> (gen/sample (s/gen ::customer))
({:id #uuid "d18896f1-6199-42bf-9be3-3d0652583902", :age 1}
 {:id #uuid "b6209798-4ffa-4e20-9a76-b3a799a31ec6", :age 2}
 {:id #uuid "6f9c6400-8d79-417c-bc62-6b4557f7d162", :age 1}
 {:id #uuid "47b71396-1b5f-4cf4-bd80-edf4792300c8", :age 2}
 {:id #uuid "808692b9-0698-4fb8-a0c5-3918e42e8f37", :age 2}
 {:id #uuid "ba663f0a-7c99-4967-a2df-3ec6cb04f514", :age 1}
 {:id #uuid "8521b611-c38c-4ea9-ae84-35c8a2d2ff2f", :age 4}
 {:id #uuid "c559d48d-4c50-438f-846c-780cdcdf39d5", :age 3}
 {:id #uuid "03c2c114-03a0-4709-b9dc-6d326a17b69d", :age 40}
 {:id #uuid "14715a50-81c5-48e4-bffe-e194631bb64b", :age 4})
user> (binding [*idgen* (let [idpool (gen/sample (s/gen :customer/id) 5)] (gen/elements idpool))] (gen/sample (s/gen ::customer)))
({:id #uuid "3e64131d-e7ad-4450-993d-fa651339df1c", :age 2}
 {:id #uuid "575b2bef-956d-4c42-bdfa-982c7756a33c", :age 1}
 {:id #uuid "575b2bef-956d-4c42-bdfa-982c7756a33c", :age 1}
 {:id #uuid "3e64131d-e7ad-4450-993d-fa651339df1c", :age 1}
 {:id #uuid "1a2eafed-8242-4229-b432-99edb361569d", :age 3}
 {:id #uuid "1a2eafed-8242-4229-b432-99edb361569d", :age 1}
 {:id #uuid "05bd521a-26f9-46e0-8b26-f798e0bf0452", :age 3}
 {:id #uuid "575b2bef-956d-4c42-bdfa-982c7756a33c", :age 19}
 {:id #uuid "31b80714-7ae0-40a0-b932-f7b5f078f2ad", :age 2}
 {:id #uuid "05bd521a-26f9-46e0-8b26-f798e0bf0452", :age 5})
user>  

A little clumsier than what you wanted, but maybe this is adequate.

You are probably better off using binding rather than with-redefs since binding modifies thread-local bindings, whereas with-redefs changes the root binding.

Since this is for generating bad test data, I'd consider avoiding the use of dynamic vars and binding altogether and just use a different spec that is only local to the test env.

dorab
  • 807
  • 5
  • 13