Skip to content

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.

  1. Adds validation
  2. Conformance
  3. Documentation
  4. 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 with s/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 the s/exercise for sample value and conformed and s/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.

See also (generated)