├── .dir-locals.el ├── .gitignore ├── test └── practicalli │ └── spec_generative_testing_test.clj ├── src └── practicalli │ ├── card_game.clj │ ├── card_game_specifications.clj │ └── spec_generative_testing.clj ├── README.md └── deps.edn /.dir-locals.el: -------------------------------------------------------------------------------- 1 | ((clojure-mode . ((cider-clojure-cli-aliases . ":env/test")))) 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | *.jar 5 | *.class 6 | /.cpcache 7 | /.lein-* 8 | /.nrepl-history 9 | /.nrepl-port 10 | -------------------------------------------------------------------------------- /test/practicalli/spec_generative_testing_test.clj: -------------------------------------------------------------------------------- 1 | (ns practicalli.spec-generative-testing-test 2 | (:require [clojure.test :refer [deftest is testing]] 3 | [practicalli.spec-generative-testing :as SUT])) 4 | -------------------------------------------------------------------------------- /src/practicalli/card_game.clj: -------------------------------------------------------------------------------- 1 | (ns practicalli.card-game) 2 | 3 | 4 | 5 | (defn regulation-card-deck 6 | [{:keys [::deck ::players] :as game}] 7 | (apply + (count deck) 8 | (map #(-> % ::delt-hand count) players))) 9 | 10 | 11 | (defn deal-cards 12 | [game] 13 | game) 14 | 15 | 16 | (defn winning-hand? 17 | [players] 18 | ;; calculate winning hand from each of players hands 19 | ;; return player 20 | #:practicalli.player-won 21 | {:name "Jenny Nada", 22 | :score 225, 23 | :delt-hand [[9 :hearts] [4 :clubs] [8 :hearts] [10 :clubs] [:queen :spades]]} 24 | 25 | ) 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spec-generative-testing 2 | Library of examples using Generative Testing with Clojure Spec 3 | 4 | ## Usage 5 | 6 | Open in a Clojure editor and start a REPL. 7 | 8 | 9 | ## Packaging and deployment 10 | 11 | Build a deployable jar of this library: 12 | 13 | $ clojure -A:jar 14 | 15 | Install it locally: 16 | 17 | $ clojure -A:install 18 | 19 | Deploy it to Clojars -- needs `CLOJARS_USERNAME` and `CLOJARS_PASSWORD` environment variables: 20 | 21 | $ clojure -A:deploy 22 | 23 | ## License 24 | 25 | Copyright © 2020 Practicalli 26 | 27 | Distributed under the Creative Commons Attribution Share-Alike 4.0 International 28 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths 2 | ["src" "resources"] 3 | 4 | :deps 5 | {org.clojure/clojure {:mvn/version "1.10.1"}} 6 | 7 | :aliases 8 | {:env/test 9 | {:extra-paths ["test"] 10 | :extra-deps {org.clojure/test.check {:mvn/version "1.0.0"}}} 11 | 12 | :test/runner 13 | {:extra-deps {com.cognitect/test-runner 14 | {:git/url "https://github.com/cognitect-labs/test-runner" 15 | :sha "f7ef16dc3b8332b0d77bc0274578ad5270fbfedd"}} 16 | :main-opts ["-m" "cognitect.test-runner" 17 | "-d" "test"]} 18 | 19 | :project/jar 20 | {:extra-deps {seancorfield/depstar {:mvn/version "1.0.94"}} 21 | :main-opts ["-m" "hf.depstar.jar" "spec-generative-testing.jar"]} 22 | 23 | :project/install 24 | {:extra-deps {deps-deploy {:mvn/version "0.0.9"}} 25 | :main-opts ["-m" "deps-deploy.deps-deploy" "install" "spec-generative-testing.jar"]} 26 | 27 | :project/deploy 28 | {:extra-deps {deps-deploy {:mvn/version "0.0.9"}} 29 | :main-opts ["-m" "deps-deploy.deps-deploy" "deploy" "spec-generative-testing.jar"]}}} 30 | -------------------------------------------------------------------------------- /src/practicalli/card_game_specifications.clj: -------------------------------------------------------------------------------- 1 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 2 | ;; Specifications for a simple card game 3 | ;; 4 | ;; Author(s): John Stevenson 5 | ;; 6 | ;; Specifications and custom predicate functions for 7 | ;; a simple playing card game 8 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 9 | 10 | 11 | (ns practicalli.card-game-specifications 12 | (:require 13 | [practicalli.card-game :as card-game] 14 | [clojure.spec.alpha :as spec] 15 | [clojure.spec.test.alpha :as spec-test])) 16 | 17 | 18 | 19 | ;; Card Specifications 20 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 21 | 22 | ;; Custom predicate functions to be used as specifications 23 | 24 | (def suit? #{:clubs :diamonds :hearts :spades}) 25 | (def rank? (into #{:jack :queen :king :ace} (range 2 11))) 26 | 27 | 28 | (spec/def ::playing-card (spec/tuple rank? suit?)) 29 | 30 | 31 | ;; Player specifications 32 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 33 | 34 | (spec/def ::name string?) 35 | (spec/def ::score int?) 36 | 37 | (spec/def ::player 38 | (spec/keys :req [::name ::score ::delt-hand])) 39 | 40 | 41 | ;; Game specifications 42 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 43 | 44 | (spec/def ::deck (spec/* ::playing-card)) 45 | 46 | (spec/def ::players (spec/* ::player)) 47 | 48 | (spec/def ::game (spec/keys :req [::players ::deck])) 49 | 50 | 51 | 52 | ;; Function Specifications 53 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 54 | 55 | (spec/fdef deal-cards 56 | :args (spec/cat :game ::game) 57 | :ret ::game 58 | :fn #(= (card-game/regulation-card-deck (-> % :args :game)) 59 | (card-game/regulation-card-deck (-> % :ret)))) 60 | 61 | 62 | (spec/fdef winning-player 63 | :args (spec/cat :players ::players) 64 | :ret ::player) 65 | 66 | ;; Instrument functions 67 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 68 | 69 | ;; Define a collection of functions to instrument 70 | 71 | (def ^:private function-specifications 72 | [`card-game/deal-cards 73 | `card-game/winning-player]) 74 | 75 | ;; simple helper functions 76 | 77 | (defn instrument-all-functions 78 | [] 79 | (spec-test/instrument function-specifications)) 80 | 81 | (defn unstrument-all-functions 82 | [] 83 | (spec-test/unstrument function-specifications)) 84 | -------------------------------------------------------------------------------- /src/practicalli/spec_generative_testing.clj: -------------------------------------------------------------------------------- 1 | (ns practicalli.spec-generative-testing 2 | (:require 3 | [clojure.spec.alpha :as spec] 4 | [clojure.spec.gen.alpha :as spec-gen] 5 | [clojure.spec.test.alpha :as spec-test])) 6 | 7 | ;; Set up project 8 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 9 | ;; clj-new now adds org.clojure/test.check as a dependency under the :test alias 10 | 11 | ;; add a dir-locals.el file and add the :test alias when running the repl from Emacs 12 | ;; ((clojure-mode . ((cider-clojure-cli-global-options . "-A:test")))) 13 | 14 | 15 | 16 | ;; Generators 17 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 18 | 19 | 20 | (spec-gen/generate (spec/gen int?)) 21 | 22 | (spec-gen/generate (spec/gen nil?)) 23 | 24 | (spec-gen/sample (spec/gen string?)) 25 | 26 | 27 | (spec-gen/generate (spec/gen #{:club :diamond :heart :spade})) 28 | 29 | (spec-gen/sample (spec/gen #{:club :diamond :heart :spade})) 30 | 31 | (spec-gen/sample (spec/gen (spec/cat :k keyword? :ns (spec/+ number?)))) 32 | 33 | 34 | 35 | 36 | ;; Player specification 37 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 38 | 39 | 40 | (def suit? #{:clubs :diamonds :hearts :spades}) 41 | (def rank? (into #{:jack :queen :king :ace} (range 2 11))) 42 | 43 | (spec/def ::playing-card (spec/tuple rank? suit?)) 44 | (spec/def ::delt-hand (spec/* ::playing-card)) 45 | 46 | (spec/def ::name string?) 47 | (spec/def ::score int?) 48 | (spec/def ::player (spec/keys :req [::name ::score ::delt-hand])) 49 | 50 | 51 | (spec/def ::deck (spec/* ::playing-card)) 52 | (spec/def ::players (spec/* ::player)) 53 | (spec/def ::game (spec/keys :req [::players ::deck])) 54 | 55 | 56 | ;; generating a random player in our card game? 57 | 58 | (spec-gen/generate (spec/gen ::player)) 59 | ;; => #:practicalli.spec-generative-testing{:name "Yp34KE63vALOeriKN4cBt", :score 225, :delt-hand ([9 :hearts] [4 :clubs] [8 :hearts] [10 :clubs] [:queen :spades] [3 :clubs] [6 :hearts] [8 :hearts] [7 :diamonds] [:king :spades] [:ace :diamonds] [2 :hearts] [4 :spades] [2 :clubs] [6 :clubs] [8 :diamonds] [6 :spades] [5 :spades] [:queen :clubs] [:queen :hearts] [6 :spades])} 60 | 61 | 62 | 63 | (spec-gen/generate (spec/gen ::game)) 64 | 65 | 66 | 67 | ;; Specification for function definition 68 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 69 | 70 | (defn regulation-card-deck 71 | [{:keys [::deck ::players] :as game}] 72 | (apply + (count deck) 73 | (map #(-> % ::delt-hand count) players))) 74 | 75 | (defn deal-cards 76 | [game] 77 | game) 78 | 79 | (spec/fdef deal-cards 80 | :args (spec/cat :game ::game) 81 | :ret ::game 82 | :fn #(= (regulation-card-deck (-> % :args :game)) 83 | (regulation-card-deck (-> % :ret)))) 84 | 85 | 86 | 87 | ;; Testing with spec 88 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 89 | 90 | ;; Generate 1000 values to run against the specification 91 | ;; takes over a minute - are all generative tests this slow? 92 | ;; if so, then generative tests should be run less often 93 | 94 | ;; spec/check takes a fully-qualified symbol so we use ` here to resolve it in the context of the current namespace. 95 | 96 | 97 | (spec-test/check `deal-cards) 98 | 99 | ;; => ({:spec #object[clojure.spec.alpha$fspec_impl$reify__2524 0x26debeba "clojure.spec.alpha$fspec_impl$reify__2524@26debeba"], 100 | ;; :clojure.spec.test.check/ret 101 | ;; {:result true, :pass? true, :num-tests 1000, :time-elapsed-ms 75054, :seed 1591928968683}, 102 | ;; :sym practicalli.spec-generative-testing/deal-cards}) 103 | 104 | 105 | 106 | 107 | ;; How to run a specific number of tests 108 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 109 | ;; clojure.spec.test.alpha/check API reference describes a second argument to the function, 110 | ;; a hash-map that includes options, one of which is :num-tests 111 | ;; The key for the options is a fully qualified namespace, specifically 112 | ;; :clojure.spec.test.check/opts 113 | ;; https://clojure.github.io/spec.alpha/clojure.spec.test.alpha-api.html#clojure.spec.test.alpha/check 114 | 115 | ;; Run a single check - very quick 116 | 117 | (spec-test/check 118 | `deal-cards 119 | {:clojure.spec.test.check/opts {:num-tests 1}}) 120 | 121 | ;; => ({:spec #object[clojure.spec.alpha$fspec_impl$reify__2524 0x26debeba "clojure.spec.alpha$fspec_impl$reify__2524@26debeba"], 122 | ;; :clojure.spec.test.check/ret 123 | ;; {:result true, :pass? true, :num-tests 1, :time-elapsed-ms 0, :seed 1591961775784}, 124 | ;; :sym practicalli.spec-generative-testing/deal-cards}) 125 | 126 | ;; Run 10 checks - very quick 127 | 128 | (spec-test/check 129 | `deal-cards 130 | {:clojure.spec.test.check/opts {:num-tests 10}}) 131 | 132 | ;; => ({:spec #object[clojure.spec.alpha$fspec_impl$reify__2524 0x26debeba "clojure.spec.alpha$fspec_impl$reify__2524@26debeba"], 133 | ;; :clojure.spec.test.check/ret {:result true, :pass? true, :num-tests 10, :time-elapsed-ms 6, :seed 1591961778220}, 134 | ;; :sym practicalli.spec-generative-testing/deal-cards}) 135 | 136 | 137 | ;; Run 100 checks - takes about 3 seconds 138 | 139 | (spec-test/check 140 | `deal-cards 141 | {:clojure.spec.test.check/opts {:num-tests 101}}) 142 | 143 | ;; => ({:spec #object[clojure.spec.alpha$fspec_impl$reify__2524 0x26debeba "clojure.spec.alpha$fspec_impl$reify__2524@26debeba"], 144 | ;; :clojure.spec.test.check/ret 145 | ;; {:result true, :pass? true, :num-tests 101, :time-elapsed-ms 2148, :seed 1591961780863}, 146 | ;; :sym practicalli.spec-generative-testing/deal-cards}) 147 | 148 | 149 | 150 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 151 | ;; Using an alias 152 | ;; The API documentation talks about using ::stc/opts 153 | ;; Suggestion from Sean Corfield on how to create such an alias 154 | ;; as the namespace does not exist 155 | ;; (alias 'stc (create-ns 'clojure.spec.test.check)) 156 | 157 | ;; ::stc/opts 158 | 159 | ;; (spec-test/check `deal-cards 160 | ;; {::stc/opts {:num-tests 1}}) 161 | 162 | ;; Code seems much cleaner if :clojure.spec.test.check/opts is used as the key name 163 | 164 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 165 | 166 | 167 | 168 | ;; Test reports 169 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 170 | 171 | (spec-test/summarize-results 172 | (spec-test/check `deal-cards 173 | {:clojure.spec.test.check/opts {:num-tests 10}})) 174 | 175 | 176 | 177 | 178 | ;; Adding more functions and specifications 179 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 180 | 181 | ;; Define a function to calculate the winning hand of a game 182 | 183 | (defn winning-player 184 | [players] 185 | ;; calculate winning player from each of players hands 186 | ;; return player 187 | ) 188 | 189 | 190 | ;; Initially we can return an explicit player data structure 191 | (defn winning-player 192 | [players] 193 | ;; calculate winning player from each of players hands 194 | ;; return player 195 | #:practicalli.player-won 196 | {:name "Jenny Nada", 197 | :score 225, 198 | :delt-hand [[9 :hearts] [4 :clubs] [8 :hearts] [10 :clubs] [:queen :spades]]}) 199 | 200 | 201 | ;; rather than explicitly add the data, we can generate the data 202 | ;; until we write the real algorithm 203 | 204 | 205 | (spec-gen/generate (spec/gen ::player)) 206 | 207 | 208 | ;; Then 209 | (defn winning-player 210 | [players] 211 | ;; calculate winning player from each of players hands 212 | ;; return player 213 | 214 | (spec-gen/generate (spec/gen ::player))) 215 | 216 | ;; This is just a temporary place holder until the algorithm of the function 217 | ;; is created 218 | 219 | ;; call winning players with mock data (not checked if not instrumented) 220 | ;; and get a generated player value back. 221 | #_(winning-player "mock data") 222 | --------------------------------------------------------------------------------