#+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