Skip to content

Instantly share code, notes, and snippets.

@mrcnc
Last active December 13, 2017 10:07
Show Gist options
  • Save mrcnc/44a0257818f8932085f398ca20abe7ba to your computer and use it in GitHub Desktop.
Save mrcnc/44a0257818f8932085f398ca20abe7ba to your computer and use it in GitHub Desktop.
clojure.spec at NOFUN
(ns nofun.spec.core
(:gen-class)
(:require [clojure.spec :as s]
[clojure.spec.gen :as gen]
[com.gfredericks.test.chuck.generators :as gen']
[camel-snake-kebab.core :refer :all]
[camel-snake-kebab.extras :refer [transform-keys]]))
;; require the namespace
(require '[clojure.spec :as s])
;; check if a spec is valid with s/valid?
(s/valid? even? 0)
(s/valid? even? 1)
(s/valid? (s/and pos? even?) 0)
(s/valid? (s/and pos? even?) 2)
(def hack-night-regex #"[(sl)|(h)]+ack night")
(s/valid? #(re-matches hack-night-regex %) "hack night")
;; define a spec in global registry with s/def
(s/def ::hack-night-spec #(re-matches hack-night-regex %))
;; sets can also be used as specs
(s/def ::suit #{:club :diamond :heart :spade})
(s/valid? ::suit :heart)
;; here's how we might spec a user
(def email-regex #"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}")
(s/def ::user/email (s/and string? #(re-matches email-regex %)))
(s/def ::user/password string?)
(s/def ::user/first-name string?)
(s/def ::user/last-name string?)
;; spec a map with required and optional keys
(s/def ::user-spec
(s/keys :req-un [::user/email ::user/password]
:opt-un [::user/first-name ::user/last-name]))
;; if a spec is invalid you can get the reason why with s/explain
(def user {:first-name "Marc"
:last-name "Cenac"
:email "mcenac@boundlessgeo.com"})
(s/explain ::user-spec user)
(s/valid? ::user-spec (assoc user :password "testpass"))
(require '[clojure.spec.gen :as gen])
;; s/gen will get the generator for a spec
(s/gen string?)
;; gen/generate will generate a single value using the spec's generator
(gen/generate (s/gen string?))
(gen/generate (gen/string-alphanumeric))
(gen/generate (gen/any))
(gen/generate (s/gen ::user-spec))
;; use this library to generate strings from regex
(require '[com.gfredericks.test.chuck.generators :as gen'])
(gen/generate (gen'/string-from-regex email-regex))
(def sql-column-regex #"[a-z][a-z0-9_]*")
;; gen/sample will generate many values
(gen/sample (gen'/string-from-regex sql-column-regex))
;; custom generators
(s/def ::sql-column (s/with-gen
#(> (count %) 5) ;; custom spec
#(gen'/string-from-regex sql-column-regex))) ;; fn returning a generator
(gen/sample (s/gen ::sql-column))
;; sometimes your predicate isn't specific enough
;; (gen/generate (s/gen ::user-spec))
;; fmap takes a function to apply to each sample generated by the generator,
;; which is the second argument to fmap
;(defn email-generator []
; (gen/fmap
; (fn [[name domain-name tld]] (str name "@" domain-name "." tld))
; (gen/tuple (gen/string-alphanumeric) (gen/string-alphanumeric) (gen/string-alphanumeric))))
;
;(s/def ::user/email
; (s/with-gen
; #(re-matches email-regex %)
; email-generator))
(gen/generate (s/gen ::user/email))
(s/def ::user/email
(s/with-gen
#(re-matches email-regex %)
#(gen'/string-from-regex email-regex)))
;; improve password spec
(s/def ::user/password (s/and string? #(s/int-in-range? 10 21 (count %))))
(s/def ::user-spec
(s/keys :req-un [::user/email ::user/password]
:opt-un [::user/first-name ::user/last-name]))
(s/valid? ::user-spec (assoc user :password "testpass1234!"))
(gen/sample (s/gen ::user-spec))
;; create a test function
(defn find-user-by-id [id]
;; pretend we saved to the db and got back a row
(let [row {:id id
:first_name "Marc"
:last_name "Cenac"
:email "mcenac@boundlessgeo.com"
:password "hashed-password"
:updated_at "2017-02-06T22:45:26.966625000-00:00"}]
(dissoc row :password :updated_at)))
;; spec the test function
(s/fdef find-user-by-id
:args (s/cat :id int?)
:ret #(not (contains? % :password))
:fn #(= (-> % :args :id) (-> % :ret :id)))
(find-user-by-id 123)
(s/exercise-fn `find-user-by-id)
;; generate tests
(require '[clojure.spec.test :as stest])
(stest/check `find-user-by-id)
(stest/check `find-user-by-id {:clojure.spec.test.check/opts {:num-tests 5000}})
(require '[camel-snake-kebab.core :refer :all])
(require '[camel-snake-kebab.extras :refer [transform-keys]])
(defn create-user [user]
;; pretend we saved to the db and got back a row
(let [row {:id 123
:first_name (:first-name user)
:last_name (:last-name user)
:email (:email user)
:password "hashed-password"
:updated_at "2017-02-06T22:45:26.966625000-00:00"}]
(transform-keys ->kebab-case-keyword (dissoc row :id :password :updated_at))))
(defn does-not-contain-password?
[user]
;(println "does this user have a pwd" user)
(not (contains? user :password)))
(s/fdef create-user
:args (s/cat :user ::user-spec)
:ret does-not-contain-password?
;:ret #(not (contains? % :password)))
;; here we're asserting that the input and output emails must match
:fn #(= (-> % :args :user :email) (-> % :ret :email)))
;; turn on instrumentation to validate the args
(stest/instrument `create-user)
(create-user user)
(create-user (assoc user :password "testpass1234!"))
;; disable instrumentation
(stest/unstrument `create-user)
;; generate tests for the function
(stest/check `create-user)
(stest/summarize-results (stest/check `create-user))
(gen/sample (s/gen ::user-spec))
(s/exercise-fn `create-user)
(stest/enumerate-namespace `nofun.spec.core)
(defn -main []
(println "Hello World"))
(defproject nofun-spec "0.0.1-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.9.0-alpha14"]
[com.gfredericks/test.chuck "0.2.7"]
[camel-snake-kebab "0.4.0"]]
:main nofun.spec.core)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment