Card game: spec and generative testing
A card game that ,,, with Specifications defined for the card data and functions that define the algorithms of the game.
Create a project
Create a new Clojure project using clj-new
tool for Clojure Tools.
clojure -A:new app practicalli/card-game
Hint::Use practicalli/clojure-deps-edn to add common tools
fork and clone the practicalli/clojure-deps-edn GitHub repository to
~/.clojure/
and instantly have access to dozens of tools for Clojure software development
Add the Clojure spec namespace
Open the src/practicalli/card_game.clj
file and require the clojure.spec.alpha
namespace
(ns practicalli.card-game.clj
(:require [clojure.spec.alpha :as spec]))
Representing playing cards
A playing card has a face value and a suit. There are 4 suits in a card deck.
A specification for the possible suits can be defined using literal values
(spec/def ::suits #{:clubs :diamonds :hearts :spades})
As the set is a predicate then it could just be bound to a name, i.e. (def suits? #{:clubs :diamonds :hearts :spades})
Representing different aspects of card game decks
Suits from different regions are called by different names. Each of these suits can be their own spec.
(spec/def ::suits-french #{:hearts :tiles :clovers :pikes})
(spec/def ::suits-german #{:hearts :bells :acorns :leaves})
(spec/def ::suits-spanish #{:cups :coins :clubs :swords})
(spec/def ::suits-italian #{:cups :coins :clubs :swords})
(spec/def ::suits-swiss-german #{:roses :bells :acorns :shields})
A composite specification called ::card-suits
provides a simple abstraction over all the variations of suits. Using ::card-suits
will be satisfied with any region specific suits.
(spec/def ::card-suits
(spec/or :french ::suits-french
:german ::suits-german
:spanish ::suits-spanish
:italian ::suits-italian
:swiss-german ::suits-swiss-german
:international ::suits-international))
Define an alias for a specification
Jack queen king are called face cards in the USA and occasionally referred to as court cards in the UK.
Define a spec for ::face-cards and then make ::court-cards and alias for ::face-cards
(spec/def ::face-cards #{:jack :queen :king :ace})
(spec/def ::court-cards ::face-cards)
Any value that conforms to the ::face-card
specification also conforms to the ::court-cards
specification.
(spec/conform ::court-cards :ace)
Playing card rank
Each suit in the deck has the same rank of cards explicitly defining a rank
(spec/def ::rank #{:ace 2 3 4 5 6 7 8 9 10 :jack :queen :king})
rank can be defined more succinctly with the clojure.core/range
function. The expression (range 2 11)
will generates a sequence of integer numbers from 2 to 10 (the end number is exclusive, so 11 is not in the sequence).
Using clojure.core/into
this range of numbers can be added to the face card values.
(into #{:ace :jack :queen :king} (range 2 11))
The ::rank
specification now generates all the possible values for playing cards.
(spec/def ::rank (into #{:ace :jack :queen :king} (range 2 11)))
The specification only checks to see if a value is in the set, the order of the values in the set is irrelevant.
Viewing Specifications
The Clojure doc
function will show specifications details.
(clojure.repl/doc ::rank)
;; :practicalli.card-game-specifications/rank
;; Spec
;; (into #{:king :queen :ace :jack} (range 2 11))
When adding a specification to a function definition, doc
will also show the specification details along with the function doc-string.
Playing Cards
A playing card is a combination of suit and face value, a pair of values, referred to as a tuple.
Clojure spec has a tuple
function, however, we need to define some predicates first
(spec/def ::playing-card (spec/tuple ::rank ::suits ))
Use the spec with values to see if they conform. Try you own values for a playing card.
(spec/conform ::playing-card [:ace :spades])
Generative testing
Mock and test data values can be generated from the specifications defined.
Add the clojure.spec.gen.alpha
namespace to access the data generators. The clojure.spec.test.alpha
namespace is required to support getting a generator for a given specification.
(ns practicalli.card-game.clj
(:require [clojure.spec.alpha :as spec]
[clojure.spec.gen.alpha :as spec-gen]
[clojure.spec.test.alpha :as spec-test]))
To generated data based on a specification, first get a generator for a given spec,
(spec/gen ::suits)
generate
will return a value using the specific generator for the specification.
(spec-gen/generate (spec/gen ::suits))
sample
will generate a number of values from the given specification
(spec-gen/sample (spec/gen ::rank))