clojure-demos/spec-demo
Oliver Marks 5b72d063c5
continuous-integration/drone/push Build is passing Details
Refactor
2021-01-29 15:02:00 +00:00
..
resources/public Refactor 2021-01-29 15:02:00 +00:00
src Initial version. 2020-11-30 19:50:16 +00:00
deps.edn Initial version. 2020-11-30 19:50:16 +00:00
dev.cljs.edn Initial version. 2020-11-30 19:50:16 +00:00
figwheel-main.edn Initial version. 2020-11-30 19:50:16 +00:00
readme.org Initial version. 2020-11-30 19:50:16 +00:00

readme.org

Minimal clojurescript project demoing spec

Spec Intro

Spec is a library for validating data at it's core but with some more advanced features like generative testing of your functions.

At it's core it is a global namespace on how your data is structured rules about the format of the data and how to generate the data.

Below we simple define id as a string, string? is a built in function which knows how to generate a string we can then test if some data is valid using spec/valid as seen below :: is shorthand for current namespace wee could define id as :myns/id and is often preferred.

  (spec/def ::id string?)
  (spec/valid? ::id "ABC-123")  ;; true
  (spec/valid? ::id 4)  ;; false

The errors reported by spec can be quite unwieldy to fix this you can use a library called expound to get more user friendly errors.

Making Specs

Basic specs

These are some simple spec declarations for some vary basic data types.

  (spec/def ::id pos-int?)
  (spec/def ::name string?)
  (spec/def ::price decimal?)
  (spec/def ::age (spec/int-in 0 100))
  (spec/def ::colours #{"red" "blue" "green"})

Slightly more complex Specs

Spec allow you to apply multiple conditions that need to pass spec/and allow you to specify multiple conditions. we can do regex matching with #"regex-here" you can also use lambda's #(> (count %) 2) for example to make sure the value is over 2

  (spec/def ::name (spec/and string? #(> (count %) 2)))
  (spec/def ::name (spec/and string? #(> (count %) 2)))
  (spec/def ::slug (spec/and string? #"[A-Fa-f0-9\-]+"))
  (spec/def ::pos-decimal (s/and decimal? #(>= % 0)))

Specing out hashmaps

Spec allow us to compose specs, this is very useful to spec out hash maps or generate api data s/keys allows for this we then use :req-un and :opt-un for unqualified keys words ie :name or :req and :opt for qualified keywords ie :core.namespace/name using existing specs as the keys means the values will be validated against those spec's below we are validating against spec defined further up the page.

  (spec/def ::api-response (s/keys :req-un [::name ::slug]
                                   :opt-un [::age]))

Specs with custom generators

When building more precise spec's you will hit failures to generate your data, spec by default has 100 tries, you can use :gen to tell the spec how the data will be generated, when you pull in clojure test check the library contains a load of helper generator functions like gen/return which can be used to write your generator functions.

All generator functions need to return a generator gen/return gen/fmap and other can help here, in the example below we always return 123 so the value is fixed but we could compose the string or use random functions to generate any format we like.

(spec/def ::demo (spec/spec string? :gen (fn [] (gen/return "123"))))

You can do other fancy things like set frequencies that a value should appear, there is a nice guide with some examples here. https://clojure.github.io/test.check/generator-examples.html

Testing

Once you have your specs you have basically created a method to write tests, you can generate data to insert into your functions to check the results, you can also check the results against a spec.

On top of this you can insert a spec where side effects occur, make sure the api's you interact do not change.

Testing reults

You can use your specs to test your results match up to your spec, you can also use specialised function specs which will generate inputs from your specs.

  (is (spec/valid ::name "username"))

Testing functions

Using spec/fdef we can define the inputs and outputs from a functions and use instrument and check to test they work as expected.

(spec/fdef demo-function
  :args (spec/cat :value int?)
  :ret int?)

(defn demo-function [value] (+ 5 value))

(stest/instrument `demo-function)
(stest/check `demo-function)