Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is generative testing in Clojure?

I came across Generative Testing in Clojure with spec notion and would like to learn about it.

Also providing some examples would be very useful.

like image 245
Ertuğrul Çetin Avatar asked Apr 11 '17 14:04

Ertuğrul Çetin


People also ask

What is generative testing?

In a generative test the engineer describes the shape of the data, and then defines the properties that the results should have, and the test runner provides randomized data to check against.

How do you run a Clojure test?

Running Tests You can run tests by using the cljs. test/run-tests macro. This may be done in your REPL or at the end of your file. If you have many test namespaces it's idiomatic to create a test runner namespace which imports all of your test namespaces and then invokes run-tests .

What is property based testing?

What is property-based testing? Property-based tests are designed to test the aspects of a property that should always be true. They allow for a range of inputs to be programmed and tested within a single test, rather than having to write a different test for every value that you want to test.

What is clojure spec?

spec is a Clojure library to describe the structure of data and functions. Specs can be used to validate data, conform (destructure) data, explain invalid data, generate examples that conform to the specs, and automatically use generative testing to test functions.


2 Answers

As introductory reading we've got the Rationale and Overview along with the Guide which should provide you with information both about the why and the how.

If you'd like a somewhat complex example, we can take the string->semantic-version function of leiningen.release:

(defn string->semantic-version [version-string]
  "Create map representing the given version string. Returns nil if the
  string does not follow guidelines setforth by Semantic Versioning 2.0.0,
  http://semver.org/"
  ;; <MajorVersion>.<MinorVersion>.<PatchVersion>[-<Qualifier>][-SNAPSHOT]
  (if-let [[_ major minor patch qualifier snapshot]
           (re-matches
            #"(\d+)\.(\d+)\.(\d+)(?:-(?!SNAPSHOT)([^\-]+))?(?:-(SNAPSHOT))?"
            version-string)]
    (->> [major minor patch]
         (map #(Integer/parseInt %))
         (zipmap [:major :minor :patch])
         (merge {:qualifier qualifier
                 :snapshot snapshot}))))

It takes a string and tries to parse it into a program-readable map representing the version number of some artifact. A spec for it could look like:

First some dependencies

(ns leiningen.core.spec.util
  (:require
   [clojure.spec           :as spec]
   [clojure.spec.gen       :as gen]
   [miner.strgen           :as strgen]
   [clojure.spec.test      :as test]
   [leiningen.release      :as release]))

then a helper macro

(defmacro stregex
  "Defines a spec which matches a string based on a given string
  regular expression. This the classical type of regex as in the
  clojure regex literal #\"\""
  [string-regex]
  `(spec/with-gen
     (spec/and string? #(re-matches ~string-regex %))
     #(strgen/string-generator ~string-regex)))

followed by a definition of a semantic version

(spec/def ::semantic-version-string
  (stregex #"(\d+)\.(\d+)\.(\d+)(-\w+)?(-SNAPSHOT)?"))

and some helper-specs

(spec/def ::non-blank-string
  (spec/and string? #(not (str/blank? %))))
(spec/def ::natural-number
  (spec/int-in 0 Integer/MAX_VALUE))

for the definition of the keys in the resulting map

(spec/def ::release/major     ::natural-number)
(spec/def ::release/minor     ::natural-number)
(spec/def ::release/patch     ::natural-number)
(spec/def ::release/qualifier ::non-blank-string)
(spec/def ::release/snapshot  #{"SNAPSHOT"})

and the map itself

(spec/def ::release/semantic-version-map
  (spec/keys :req-un [::release/major ::release/minor ::release/patch
                      ::release/qualifier ::release/snapshot]))

followed by the function spec:

(spec/fdef release/string->semantic-version
           :args (spec/cat :version-str ::release/semantic-version-string)
           :ret  ::release/semantic-version-map)

By now we can let Clojure Spec generate test data and feed it into the function itself in order to test whether it meets the constraints we've put up for it:

(test/check `release/version-map->string)
=> ({:spec #object[clojure.spec$fspec_impl$reify__14248 0x16c2555 "clojure.spec$fspec_impl$reify__14248@16c2555"],
     :clojure.spec.test.check/ret {:result true,
                                   :num-tests 1000,
                                   :seed 1491922864713},
     :sym leiningen.release/version-map->string})

This tells us that out of the 1000 test cases spec generated for us the function passed every single one.

like image 135
Rovanion Avatar answered Nov 03 '22 16:11

Rovanion


You may find it easiest to get started looking at clojure/test.check before you dive into Clojure Spec. From the project page:

(require '[clojure.test.check :as tc])
(require '[clojure.test.check.generators :as gen])
(require '[clojure.test.check.properties :as prop])

(def sort-idempotent-prop
  (prop/for-all [v (gen/vector gen/int)]
    (= (sort v) (sort (sort v)))))

(tc/quick-check 100 sort-idempotent-prop)
;; => {:result true, :num-tests 100, :seed 1382488326530}

In prose, this test reads: for all vectors of integers, v, sorting v is equal to sorting v twice.

What happens if our test fails? test.check will try and find 'smaller' inputs that still fail. This process is called shrinking. Let's see it in action:

(def prop-sorted-first-less-than-last
  (prop/for-all [v (gen/not-empty (gen/vector gen/int))]
    (let [s (sort v)]
      (< (first s) (last s)))))

(tc/quick-check 100 prop-sorted-first-less-than-last)
;; => {:result false, :failing-size 0, :num-tests 1, :fail [[3]],
       :shrunk {:total-nodes-visited 5, :depth 2, :result false,
                :smallest [[0]]}}
like image 45
Alan Thompson Avatar answered Nov 03 '22 14:11

Alan Thompson