87 lines
4.3 KiB
Org Mode
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
|