Specs¶
Summary¶
This is a quick summary of the official guide in Clojure. The goal is to gather most function in a single place and also acts as a reminder.
High leverage on specifying entities in Clojure code.
- Adds validation
- Conformance
- Documentation
- Generative testing
Specs are predicates and can be composed with the assigned keys. You want to add this dependency at test time for generative testing
;; deps.edn
{:aliases {:dev {:extra-deps {org.clojure/test.check {:mvn/version "1.0.0"}}}}}
Definitions¶
s/def
defines specs (with fully qualified keys). s/valid
and
s/conform
are the most important function for checking on runtime the
specifications of input/output.
(require '[clojure.spec.alpha :as s])
(s/def :image/size
(s/cat :height pos-int?
:width pos-int?
:depth pos-int?))
(s/conform :image/size [10 23 4]) ;; useful for turning tuples into maps
;; => {:height 10, :width 23, :depth 4}
(s/valid? :image/size [10 23 4]) ;; => true
(s/valid? :image/size [10 23 "s"]) ;; => false
(s/valid? :image/size [10 23 -1]) ;; => false
Composition is achieved with s/and
and s/or
(s/def ::big-even (s/and pos-int? even? #(> % 1000)))
(s/def ::identifier (s/or :name string?
:id pos-int?))
(s/conform ::identifier "abc")
(s/conform ::identifier 100)
explain
allows to have data on why a spec is failing.
Entities map¶
Entities map are defined with s/keys
and has the req
, req-un
,
opt
, opt-un
keywords argument (un
stands for unqualified keys).
Sequential keyword arguments can be defined with the s/keys*
function
which will check vector of keywords-values pair. You can use s/and
and
s/or
with the keys for a finer definition of membership of keys (for
example when a field in a map, then other keys are required and having
different element conforming). For example, either an email or a id
string is required being conform.
Collection¶
Collection has s/cat
, s/coll-of
, s/every
, s/map-of
,
s/every-kv
, s/tuple
. The last one is the most permissive as it
similar product type. every
and every-kv
are suitable for large
collection as they will not conform their value (trade off against
coll-of
and map-of
is error message are less detailed).
(s/def ::point-0 (s/tuple double? double?))
(s/def ::x double?)
(s/def ::y double?)
(s/def ::point-1 (s/tuple ::x ::y))
(s/def ::point-2 (s/cat :x double? :y double?))
(s/def ::point-3 (s/coll-of? double?))
;; additional arguments: :kind (type such as vector?)
;; :count (exact) :min-count (minimal) :max-count (maximal) :distinct (unique
;; values) :into ([] {} () #{})
Multi specs¶
You can use multimethods to define multi-specs. Useful for conforming maps depending on certain keys.
(defmulti event-type :event/type)
(defmethod event-type :event/search [_]
(s/keys :req [:event/type :event/timestamp :search/url]))
(defmethod event-type :event/error [_]
(s/keys :req [:event/type :event/timestamp :error/message :error/code]))
(s/def :event/type keyword?) ;; this will be used by multi-spec
;; the last argument is used for generative testing
(s/def :event/event (s/multi-spec event-type :event/type))
(s/valid? :event/event
{:event/type :event/search
:event/timestamp 1463970123000
:search/url "https://clojure.org"}) ;; => true
(s/explain :event/event
{:event/type :event/search
:search/url 200}) ;; fails because missing timestamp
;; 200 - failed: string? in: [:search/url]
;; at: [:event/search :search/url] spec: :search/url
;; {:event/type :event/search, :search/url 200} - failed: (contains? % :event/timestamp)
;; at: [:event/search] spec: :event/event
Sequence specs¶
- Sequences can be expressed with the regular expression operators
s/cat
,
s/alt
, s/*
, s/+
, s/?
.
-
s/alt
is difference from or as it will select one precisely and is usually use withs/cat
. -
You can
s/describe
to take the description of a spec. -
s/&
allows to combine and regex operators and additional predicates. -
s/spec
should be used for nesting regex specs.
(s/def ::nested
(s/cat :names-kw #{:names}
:names (s/spec (s/* string?))
:nums-kw #{:nums}
:nums (s/spec (s/* number?))))
(s/conform ::nested [:names ["a" "b"] :nums [1 2 3]])
;; => {:names-kw :names, :names ["a" "b"], :nums-kw :nums, :nums [1 2 3]}
(s/def ::unnested
(s/cat :names-kw #{:names}
:names (s/* string?)
:nums-kw #{:nums}
:nums (s/* number?)))
(s/conform ::unnested [:names "a" "b" :nums 1 2 3])
;; => {:names-kw :names, :names ["a" "b"], :nums-kw :nums, :nums [1 2 3]}
Validation¶
- Assertion can be done with
s/assert
, on success the value is returned on
failure assertion error is thrown.
- Default is assertion checking is off, and can be change with
s/check-asserts
. - Using
s/conform
, the result is either
conformed, or ::s/invalid
which is a special key and can be used for
throwing errors.
;; ex-info stands for exception info
(when (= (s/conform ::config input) ::s/invalid)
(throw (ex-info "Invalid input" (s/explain-data ::config input))))
Functions specs¶
s/fdef
defines spec of a function while s/fspec
allows to defines a
generic function signature. The keys are :args
, :ret
and :fn
all
accepts predicates/specs as argument. The :fn
keyword arguments is
special as it is meant for defining properties of the function for
generative testing (similar to prop/for-all
in test.check
). The
properties can be enforced on the argument (preconditions) on the result
(post conditions) or on a relationship between the arguments and the
return value or some invariant properties on the function call itself.
See Generative Testing.
Generation¶
-
The main namespace is
(require '[clojure.spec.gen.alpha :as gen])
and the function are
gen/generate
,gen/sample
allows to generate value from the specs. If you want to conformed data as well you can use thes/exercise
for sample value and conformed ands/exercise-fn
to see to see sample arguments and results. An example of generation is(require '[clojure.spec.alpha :as s]) (require '[clojure.spec.gen.alpha :as gen]) (s/def ::x pos-int?) (gen/generate (s/gen ::x)) ;; => 12734020 (gen/sample (s/gen ::x)) ;; => (1 1 2 4 4 7 3 55 4 10) (gen/sample (s/gen ::x) 3) ;; => (2 2 2) (s/def ::m (s/map-of keyword? pos-int? :min-count 2)) (s/exercise ::m 1) (let [custom-gen (gen/bind (s/gen ::m) (fn [m] (gen/tuple (gen/return m) (gen/vector-distinct (gen/elements (keys m)) {:min-elements 2}))))] (gen/sample custom-gen 10))
Custom generation¶
Three ways to create generators: spec create from the predicates, create
our own with spec.gen and use test.check
or compatible (such as
test.chuk), last options
requires runtime dependence on test check (so first two are strongly
recommended).
The main functions are gen/fmap
(returns a value from a generator) or
gen/bind
(returns a generator from a generator). Using
test.check/let
allows to use gen/bind
with sanity.
(s/def ::kws
(s/with-gen
(s/and keyword? #(= (namespace %) "my.domain"))
#(s/gen #{:my.domain/name :my.domain/occupation :my.domain/id})))
(gen/sample (s/gen ::kws))
(def kw-gen-2 (gen/fmap #(keyword "my.domain" %) (gen/string-ascii)))
(gen/sample kw-gen-2 10)
(def kw-gen-3
(gen/fmap #(keyword "my.domain" %)
(gen/such-that #(not= % "")
(gen/string-alphanumeric))))
(gen/sample kw-gen-3 5)
;; this is what we call using a model to generate our output
(s/def ::hello
(s/with-gen #(clojure.string/includes? % "hello")
#(gen/fmap (fn [[s1 s2]] (str s1 "hello" s2))
(gen/tuple (gen/string-alphanumeric)
(gen/string-alphanumeric)))))
(gen/sample (s/gen ::hello))
Testing¶
Instrumentation is to validate the input argument (the :args
key),
whereas checking is for testing with random input and all the :args
,
:ret
, :fn
keys.
(require '[clojure.spec.test.alpha :as stest])
(require '[clojure.spec.alpha :as s])
(defn ranged-rand
"Returns random int in range start <= rand < end"
[start end]
(+ start (long (rand (- end start)))))
(s/def ::int int?)
(s/fdef ranged-rand
:args (s/and (s/cat :start ::int :end ::int)
#(< (:start %) (:end %)))
:ret int?
:fn (fn [{:keys [args ret]}]
(s/and #(>= ret (:start args))
#(< ret (:end args)))))
(doc ranged-rand)
(s/exercise-fn `ranged-rand)
(stest/check `ranged-rand)
(stest/check `ranged-rand {:gen {::int #{2 5 7 10}}})
In order to check all function in a given namepsace you can use
enumerate-namespace
.
(-> (stest/enumerate-namespace 'user) stest/check)
When stest/instrument
is applied to a function, it can take options on
function and the stub keys takes a spec x
as a value which replace the
function invokation by a generated value from the spec x
. Hence it
useful for testing systems without invoking server and side effects/IO.
Tricks¶
Check membership¶
Use sets to check for membership
(s/def ::assets #{:equity :fixed-income :commodity :etf :products})
(s/valid? ::assets :equity) ; => true
(s/valid? ::assets :spx) ; => false
Check relationship between values of a map¶
(s/def ::dates (s/coll-of inst?))
(s/def ::values (s/coll-of double?))
(s/def ::timeseries
(s/and (s/keys :req-un [::dates ::values])
#(let [{:keys [dates values]} %]
(= (count dates) (count values)))))
Generic function signature¶
(defn f [x y] x)
(defn g [x y] y)
(s/def ::f (s/fspec :args (s/coll-of int?)
:ret int?))
(s/fdef f ::f)
(s/fdef g ::f)
Properties¶
See Generative Testing.
Share specs over the wire¶
You can use the s/form
function to get the definitions of the specs
(s/def ::a (s/and even? pos-int?))
(s/def ::m (s/keys :req [::a]))
(s/form ::a)
;; => (clojure.spec.alpha/and clojure.core/even? clojure.core/pos-int?)
(s/form ::m)
;; => (clojure.spec.alpha/keys :req [:user/a])
test.check¶
Generative testing¶
Using the namespace
(require '[test.check.generators :as gen]) ;; or
(require '[clojure.spec.gen.alph :as gen])
The following are combinators of simple generators
gen/vector
gen/vector-distinct
gen/tuple ;; concatenate the generator
gen/one-of ;; random choice of generator
gen/frequency ;; distribution of generators
gen/such-that ;; conditions for generation
gen/fmap ;; returns a value from a generator
gen/bind ;; returns a new generator
gen/let ;; sane macro for using bind.
Links¶
- https://github.com/clojure/test.check, quick check implementation in Clojure.
- https://github.com/clojure/test.check/blob/master/doc/cheatsheet.md
- https://github.com/metosin/spec-tools, tools for clojure.spec
- https://github.com/jeaye/orchestra, complete instrumentation for clojure.spec.
- https://github.com/bhb/expound, improved specs errors message.
- https://github.com/bhauman/spell-spec, spell checker for keys in map entities.
- https://github.com/reifyhealth/specmonstah, simulation of database state given specs and a schema.
- https://github.com/stathissideris/spec-provider, infer clojure specs from sample data.
- https://github.com/gnl/ghostwheel sane notation for fdef.
- Clojure cheat sheet
- https://blog.taylorwood.io/2018/10/15/clojure-spec-faq.html, good FAQ
- https://blog.taylorwood.io/2017/10/15/fspec.html , good details about fspec