Generator management utility for clojure.spec
- Generator definitions isolated from spec definitions
- No more need to mess up your spec definition with a gigantic
s/with-gen
code!
- No more need to mess up your spec definition with a gigantic
- Provides switching mechanism between multiple generator implementations for a single spec
Add the following to your :dependencies
:
defgenerator
/gen
with-gen-group
/use-gen-group
merge-groups
/extend-group
def-gen-group
->overrides-map
First, define a generator which you want to use via Genman as follows:
(require '[clojure.spec.alpha :as s]
'[clojure.test.check.generators :as gen]
'[genman.core :as genman :refer [defgenerator]])
(s/def ::id int?)
(s/def ::name string?)
(defgenerator ::id
(s/gen #{0 1 2}))
Once a generator is defined using defgenerator
, you can use it with genman/gen
instead of s/gen
:
(gen/generate (genman/gen ::id))
;; => 0
(gen/generate (genman/gen ::id))
;; => 2
(gen/generate (genman/gen ::id))
;; => 1
In addition, Genman provides switching mechanism between multiple generator implementations for a single spec. To use this facility, a generator should be defined in a generator group. In the example below, two more implementations for each ::id
and ::name
are being defined in the generator groups named :dev
and :test
:
(genman/with-gen-group :dev
(defgenerator ::id
(gen/return 42))
(defgenerator ::name
(gen/return "foo")))
(genman/with-gen-group :test
(defgenerator ::id
(gen/return 101))
(defgenerator ::name
(gen/return "bar")))
And then, specify the generator group with with-gen-group
to choose those specific implementations:
(genman/with-gen-group :dev
(gen/generate (genman/gen (s/tuple ::id ::name))))
;; => [42 "foo"]
(genman/with-gen-group :test
(gen/generate (genman/gen (s/tuple ::id ::name))))
;; => [101 "bar"]
If no generator group is specified, genman/gen
and defgenerator
will behave as if the :default
generator group were specified.
Note that since with-gen-group
is built on top of dynamic var binding, once a generator got out of that scope, it could lose the effect of specifying the generator group:
(def g
(genman/with-gen-group :dev
(fn []
(genman/gen (s/tuple ::id ::name)))))
(gen/generate (g))
;; => [0 ""]
(gen/generate (g))
;; => [2 "O2ltmsM"]
To avoid this behavior, use use-gen-group
instead:
(def g
(genman/use-gen-group :dev
(fn []
(genman/gen (s/tuple ::id ::name)))))
(gen/generate (g))
;; => [42 "foo"]
(gen/generate (g))
;; => [42 "foo"]
As a rule of thumb, with-gen-group
works well for test fixtures, and use-gen-group
suits for use in each (property-based) test case.
Also, there are some ways to create a new generator group based on existing ones (which we call an adhoc generator group).
merge-groups
merges more than one generator groups (in the left-to-right manner as with clojure.core/merge
):
(genman/with-gen-group :test1
(defgenerator ::id
(gen/return 42))
(defgenerator ::name
(gen/return "foo")))
(genman/with-gen-group :test2
(defgenerator ::name
(gen/return "bar")))
(genman/with-gen-group (genman/merge-groups :test1 :test2)
(gen/generate (genman/gen (s/tuple ::id ::name))))
;; => [42 "bar"]
Or, you can simply pass a map, from spec name keys to fns returning a generator, to override an existing generator group:
(genman/with-gen-group (genman/merge-groups :test1 {::name #(gen/return "baz")})
(gen/generate (genman/gen (s/tuple ::id ::name))))
;; => [42 "baz"]
If you would like to wrap the existing generator implementation instead, extend-group
would be useful:
(genman/with-gen-group (genman/extend-group :test1 {::id (fn [g] (gen/fmap #(* % 100) g))})
(gen/generate (genman/gen ::id)))
;; => 4200
Although adhoc generator groups have no need to have their own name, occasionally it's convenient to reference them with a fixed name to reuse them in several places. To define a name for an adhoc generator group, use def-gen-group
as follows:
(require '[genman.core :as genman :refer [def-gen-group]])
(genman/with-gen-group :dev
(defgenerator ::id
(s/gen #{0 1 2})))
(def-gen-group :dev'
(genman/extend-group :dev
{::id (fn [g] (gen/such-that even? g))}))
(genman/with-gen-group :dev'
(gen/generate (genman/gen ::id)))
;; => 2
(genman/with-gen-group :dev'
(gen/generate (genman/gen ::id)))
;; => 0
A named adhoc generator group can be used in exactly the same way as ordinary generator groups.
Sometimes there are situations where you would like to cooperate with clojure.spec APIs such as clojure.spec.test.alpha/check
and clojure.spec.test.alpha/instrument
, enabling a generator group. In those cases, ->overrides-map
would do to make their :gen
optional argument:
(require '[clojure.spec.test.alpha :as st])
(s/def ::amount nat-int?)
(defn double-amount [amount]
(* 2 amount))
(s/fdef double-amount
:args (s/cat :amount ::amount)
:ret ::amount)
(genman/with-gen-group :dev
(defgenerator ::amount
(s/gen (s/int-in 0 10000))))
(st/check `double-amount)
;; throws an ArithmeticException (integer overflow)
(st/check `double-amount {:gen (genman/->overrides-map :dev)})
;; pass the check
Copyright © 2018 Shogo Ohta
Distributed under the Eclipse Public License version 1.0.