clojure-demos/spec-demo/readme.org

87 lines
4.3 KiB
Org Mode

#+TITLE: 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.
#+BEGIN_SRC clojurescript :tangle ./src/test.cljs
(spec/def ::id string?)
(spec/valid? ::id "ABC-123") ;; true
(spec/valid? ::id 4) ;; false
#+END_SRC
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.
#+BEGIN_SRC clojurescript :tangle ./src/test.cljs
(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"})
#+END_SRC
** 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
#+BEGIN_SRC clojurescript :tangle ./src/test.cljs
(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)))
#+END_SRC
** 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.
#+BEGIN_SRC clojurescript :tangle ./src/test.cljs
(spec/def ::api-response (s/keys :req-un [::name ::slug]
:opt-un [::age]))
#+END_SRC
** 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.
#+BEGIN_SRC clojurescript :tangle ./src/test.cljs
(spec/def ::demo (spec/spec string? :gen (fn [] (gen/return "123"))))
#+END_SRC
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.
#+BEGIN_SRC clojurescript :tangle ./src/test.cljs
(is (spec/valid ::name "username"))
#+END_SRC
** 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.
#+BEGIN_SRC clojurescript :tangle ./src/test.cljs
(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)
#+END_SRC