Skip to content

Specifications for function definitions - fdefλ︎

Create a spec/fdef for the register-account-holder function

clojure.spec.alpha/fdef defines a specification for a function definition, defn. Specifications can attached to the arguments using :args, the return value using :ret and the relationship between the two using fn.

:args, :ret and fn are optional, although args and ret are required if you want to use :fn

Add a spec to cover the function argumentsλ︎

:args is a compound specification that covers all the function arguments. The :args spec is invoked with the arguments in a list, so working with them is like using apply.

Using regular expressions we can find the right arguments to give to the specification. Regular expression spec functions include

  • spec/cat
  • spec/alt
  • spec/*

The register-account-holder only takes one argument, so spec/cat is used to bind a local key to the specification.

The function is defined in the practicalli.banking-on-clojure namespace. Require that namespace in the current ns form.

(ns practicalli.banking-specifications
  (:require [clojure.spec.alpha :as spec]
            [clojure.spec.gen.alpha :as spec-gen]
            [clojure.spec.test.alpha :as spec-test]

            [practicalli.banking-on-clojure :as SUT]))

The SUT alias is used for the banking-on-clojure namespace, as is done with clojure.test unit test namespaces.

(spec/fdef SUT/register-account-holder
  :args (spec/cat :customer
                  :practicalli.bank-account-spec/customer-details))

Checking function calls against the spec - instrumentλ︎

spec/fdef by itself does not run checks against the specs

(register-account-holder {})
;; => {:account-id #uuid "3a6dddb7-dd87-485e-90f8-8c8975302845"}

Require the Clojure spec test library

(require '[clojure.spec.test.alpha :as spec-test])

spec/instrument will add a run time check for the specification

(spec-test/instrument `SUT/register-account-holder)

No the function is instrumented, data used as arguments of a function call will be checked against the specification.

(register-account-holder {::bad "data"})

This function call throws an exception because of the specification attached to the :args section of the fdef specification.

The error report provides detailed and quite clear information to help diagnose the issue

 1. Unhandled clojure.lang.ExceptionInfo
 Spec assertion failed.

 Spec: #object[clojure.spec.alpha$regex_spec_impl$reify__2509 0x12b66a86 "clojure.spec.alpha$regex_spec_impl$reify__2509@12b66a86"]
 Value: (#:practicalli.bank-account-design-journal{:bad "data"})

 Problems:

 val: #:practicalli.bank-account-design-journal{:bad "data"}
 in: [0]
 failed: (contains? % :practicalli.bank-account-spec/first-name)
 spec: :practicalli.bank-account-spec/customer-details
 at: [:customer]

 val: #:practicalli.bank-account-design-journal{:bad "data"}
 in: [0]
 failed: (contains? % :practicalli.bank-account-spec/last-name)
 spec: :practicalli.bank-account-spec/customer-details
 at: [:customer]

 val: #:practicalli.bank-account-design-journal{:bad "data"}
 in: [0]
 failed: (contains? % :practicalli.bank-account-spec/email-address)
 spec: :practicalli.bank-account-spec/customer-details
 at: [:customer]

 val: #:practicalli.bank-account-design-journal{:bad "data"}
 in: [0]
 failed: (contains? % :practicalli.bank-account-spec/residential-address)
 spec: :practicalli.bank-account-spec/customer-details
 at: [:customer]

 val: #:practicalli.bank-account-design-journal{:bad "data"}
 in: [0]
 failed: (contains? % :practicalli.bank-account-spec/social-security-id)
 spec: :practicalli.bank-account-spec/customer-details
 at: [:customer]

Calling the register-account-holder with a value that conforms to the bank-account-spec for customer details returns the new value for account-holder

(register-account-holder
  #:practicalli.bank-account-spec
  {:first-name          "Jenny"
   :last-name           "Jetpack"
   :email-address       "jenny@jetpack.org"
   :residential-address "42 meaning of life street, Earth"
   :postal-code         "AB3 0EF"
   :social-security-id  "123456789"})

;; => {:practicalli.bank-account-spec/first-name "Jenny", :practicalli.bank-account-spec/last-name "Jetpack", :practicalli.bank-account-spec/email-address "jenny@jetpack.org", :practicalli.bank-account-spec/residential-address "42 meaning of life street, Earth", :practicalli.bank-account-spec/postal-code "AB3 0EF", :practicalli.bank-account-spec/social-security-id "123456789", :account-id #uuid "e0f327de-4e92-479e-a9de-468e2c7c0e6d"}

Add a specification to the return valueλ︎

Attach the account-holder details specification to :ret

(spec/fdef register-account-holder
  :args (spec/cat :customer
                  :practicalli.bank-account-spec/customer-details)
  :ret :practicalli.bank-account-spec/account-holder)

If the register-account-holder logic changes to return a different value that the return spec, then an exception is raised

Returns an integer rather than a uuid

(defn register-account-holder
  "Register a new customer with the bank
  Arguments:
  - hash-map of customer-details
  Return:
  - hash-map of an account-holder (adds account id)"
  [customer-details]

  (assoc customer-details
         :practicalli.bank-account-spec/account-id
         (rand-int 100000)
         #_(java.util.UUID/randomUUID)))

So this should fail

(register-account-holder
  #:practicalli.bank-account-spec
  {:first-name          "Jenny"
   :last-name           "Jetpack"
   :email-address       "jenny@jetpack.org"
   :residential-address "42 meaning of life street, Earth"
   :postal-code         "AB3 0EF"
   :social-security-id  "123456789"})

It still works as spec-test/instrument only checks the args value.

spec-test/check will test the return value with generated tests

(require '[clojure.spec.gen.alpha :as spec-gen])
(spec-test/check `SUT/register-account-holder)

The result is 100 generated tests that all fail, because the function was changed to return integers, not uuids

 1. Caused by clojure.lang.ExceptionInfo
 Couldn't satisfy such-that predicate after 100 tries.
 {:pred      #function[clojure.spec.alpha/gensub/fn--1876],
  :gen       {:gen #function[clojure.test.check.generators/such-that/fn--8322]},
  :max-tries 100}

Change the function back againλ︎

(defn register-account-holder
  "Register a new customer with the bank
  Arguments:
  - hash-map of customer-details
  Return:
  - hash-map of an account-holder (adds account id)"
  [customer-details]

  (assoc customer-details
         :practicalli.bank-account-spec/account-id
         (java.util.UUID/randomUUID)))

Instrument the functionλ︎

Testing function calls against the specification

Requires the spec test namespace

(require '[clojure.spec.test.alpha :as spec-test])

Instrument the spec to add checking, this only checks the arguments are correct.

(spec-test/instrument `practicalli.bank-account/register-account-holder)
(register-account-holder {:first-name          "Jenny"
                          :last-name           "Jetpack"
                          :email-address       "jenny@jetpack.org"
                          :residential-address "42 meaning of life street"
                          :postal-code         "AB3 0EF"
                          :social-security-id  "123456789"})