Last active
December 13, 2017 10:07
-
-
Save mrcnc/44a0257818f8932085f398ca20abe7ba to your computer and use it in GitHub Desktop.
clojure.spec at NOFUN
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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")) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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