This repository has been archived by the owner. It is now read-only.
Property-based generative testing for stateful computations in Clojure
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
src/states
test/states
.gitignore
.travis.yml
README.md
project.clj

README.md

States

Property-based generative testing for stateful computations in Clojure.

Build Status

Clojars Project

Example: java.util.Set

In this example we will specify interactions with objects implementing the Set interface. Let's start with requiring necessary namespaces.

(require '[clojure.test.check :refer [quick-check]]
         '[clojure.test.check.generators :as gen]
         '[states.core :refer [run-commands]])

Commands which we are going to invoke on tested sets have to be identified using resolvable symbols. For that purpose we have to wrap the Java API with some vars.

(defn set-contains [set elem]
  (.contains set elem))

(defn set-add [set elem]
  (.add set elem))

(defn set-remove [set elem]
  (.remove set elem))

(defn new-set [class]
  (.newInstance class))

The commands function returns a generator. We assume that the Set under test is available in the state under key :set. If that's not the case we return a command constructing it. For the sake of simplicity we will work with sets containing exclusively non-negative integers lesser than 10.

(defn commands [{:keys [set class]}]
  (if set
    (gen/tuple (gen/elements `[set-contains set-add set-remove])
               (gen/return set)
               (gen/fmap #(mod % 10) gen/int))
    (gen/return [`new-set class])))

The next state of our computation is determined by next-step, in which we manage a control set with all elements in the tested set and keep track of the variable with the tested set. If the command is set-add we conjoin the element to the set at :elems. If the command is new-set we save the variable to which the test set is bound.

(defn next-step [state var [fn _ elem]]
  (condp = fn
    `set-add (update-in state [:elems] conj elem)
    `new-set (assoc state :set var)
    state))

The last missing function is postcondition in which we validate that set-contains returns true iff the element is in our control set at :elems. For all other commands the postcondition is always satisfied.

(defn postcondition [{:keys [elems]} [fn _ elem] value]
  (if (= fn `set-contains)
    (= value (contains? elems elem))
    true))

Finally, we build a property using run-commands and use quick-check to verify that it holds. We define the initial state to contain an empty :elems control set. We decide to test java.util.HashSet by passing it as :class.

(quick-check 1000 (run-commands commands next-step postcondition
                                {:init-state {:elems #{}
                                              :class java.util.HashSet}}))
;; => {:result
;;     #<ExceptionInfo clojure.lang.ExceptionInfo: Postcondition unsatisfied
;;       {:set var-0, :elems #{8}, :class java.util.HashSet}>,
;;     :seed 1414360515548,
;;     :failing-size 11,
;;     :num-tests 12,
;;     :fail
;;     [((set var-0 (user/new-set java.util.HashSet))
;;       (set var-1 (user/set-add var-0 8))
;;       (set var-2 (user/set-remove var-0 8))
;;       (set var-3 (user/set-contains var-0 8)))],
;;     :shrunk
;;     {:total-nodes-visited 15,
;;      :depth 0,
;;      :result
;;      #<ExceptionInfo clojure.lang.ExceptionInfo: Postcondition unsatisfied
;;        {:set var-0, :elems #{8}, :class java.util.HashSet}>,
;;      :smallest
;;      [((set var-0 (user/new-set java.util.HashSet))
;;        (set var-1 (user/set-add var-0 8))
;;        (set var-2 (user/set-remove var-0 8))
;;        (set var-3 (user/set-contains var-0 8)))]}}

It turns out that our postcondition doesn't hold if we test for presence of an element which has been removed. Why is it failing? We misspecified next-step in our model of java.util.Set; We forgot to remove elements from our control set after execution of set-remove. Let's correct this by adding another clause to condp.

(defn next-step [state var [fn _ elem]]
  (condp = fn
    `set-remove (update-in state [:elems] disj elem)
    `set-add (update-in state [:elems] conj elem)
    `new-set (assoc state :set var)
    state))

Let's retest the property.

(quick-check 1000 (run-commands commands next-step postcondition
                                {:init-state {:elems #{}
                                              :class java.util.HashSet}}))
;; => {:result true, :num-tests 100, :seed 1414360168056}

It works; The property is now satisfied.

Related work

License

Copyright (c) 2014 Jan Stępień

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.