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.

  1. 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

  1. (ns practicalli.card-game.clj
  2. (: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

  1. (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.

  1. (spec/def ::suits-french #{:hearts :tiles :clovers :pikes})
  1. (spec/def ::suits-german #{:hearts :bells :acorns :leaves})
  1. (spec/def ::suits-spanish #{:cups :coins :clubs :swords})
  1. (spec/def ::suits-italian #{:cups :coins :clubs :swords})
  1. (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.

  1. (spec/def ::card-suits
  2. (spec/or :french ::suits-french
  3. :german ::suits-german
  4. :spanish ::suits-spanish
  5. :italian ::suits-italian
  6. :swiss-german ::suits-swiss-german
  7. :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

  1. (spec/def ::face-cards #{:jack :queen :king :ace})
  1. (spec/def ::court-cards ::face-cards)

Any value that conforms to the ::face-card specification also conforms to the ::court-cards specification.

  1. (spec/conform ::court-cards :ace)

Playing card rank

Each suit in the deck has the same rank of cards explicitly defining a rank

  1. (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.

  1. (into #{:ace :jack :queen :king} (range 2 11))

The ::rank specification now generates all the possible values for playing cards.

  1. (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.

  1. (clojure.repl/doc ::rank)
  2. ;; :practicalli.card-game-specifications/rank
  3. ;; Spec
  4. ;; (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

  1. (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.

  1. (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.

  1. (ns practicalli.card-game.clj
  2. (:require [clojure.spec.alpha :as spec]
  3. [clojure.spec.gen.alpha :as spec-gen]
  4. [clojure.spec.test.alpha :as spec-test]))

To generated data based on a specification, first get a generator for a given spec,

  1. (spec/gen ::suits)

generate will return a value using the specific generator for the specification.

  1. (spec-gen/generate (spec/gen ::suits))

sample will generate a number of values from the given specification

  1. (spec-gen/sample (spec/gen ::rank))