Valid as of b9a53407fece
This library allows you to easily and quickly define specifications that a given function must follow. The fundamental idea is that a specification should be able to exist right next to it's implementation.
One thing to keep in mind is this library is not intended for testing functions that are stateful. Those are bad anyway, so I don't consider that to be a negative.
All specs (once defined) are attached to the metadata of the function var (found with (var fn)) as a single, no-arg function in the :spec slot.
It's probably easiest to describe how to use it with an example...
Spec for +
(defspec + ; Shows we are defining a specification for the + function ( (= value 0)) ; Verify that (+) => 0 ( (= value 1)) ; Verify that (+ 1) => 1 ([1 2 3] (= value 6)) ; Verify that (+ 1 2 3) => 6 ([1 'a] ; Verify that (+ 1 'a) throws a ClassCastException (instance? java.lang.ClassCastException exception) (.contains (.getMessage exception) "Symbol cannot be cast to java.lang.Number")))
As you can see, a spec is simply a vector of parameters to pass to the function, followed by at least one assertion on the return value. Each assertion simply needs to return false/nil if the assertion fails.
There are 3 variables accessible to your assertions:
- value => the value returned by the function (nil if an exception was thrown)
- exception => the exception thrown by the function (nil if no exception thrown)
- sysout => the contents of *out* during the time the function was executed
Verifying a spec
By default, defining a spec verifies that the spec is valid as it's being defined. This behaviour can be overridden a few ways:
- pass the java system parameter skipVerifySpecs (Note: any value, including false, will be interpreted as skipping the verification. We're only checking for the existance of the parameter, not the value)
# Note: The following has exactly the same behaviour: not verifying the specs as they are defined $ java -cp $CLOJURE_LIBS -DskipVerifySpecs=true clojure.lang.Script your_script.clj $ java -cp $CLOJURE_LIBS -DskipVerifySpecs=false clojure.lang.Script your_script.clj
- Rebind *should-verify-on-load* to false
(ns user (:use clojure.spec)) (binding [*should-verify-on-load* false] ; You can also use some other strategy to decide (defspec + ; when you should do the verification on definition ...
If you do disable automatic verification, you can always manually verify with verify-spec:
Viewing the specdoc
When a spec is defined on a function, the definition of that spec is also attached to the function. You can view the spec at any time with the function #'specdoc
user=> (specdoc +) ------------------------- clojure/+ ( [x] [x y] [x y & more]) Returns the sum of nums. (+) returns 0. Spec: ( (= value 0)) ( (= value 1)) ([1 2 3] (= value 6)) ([1 (quote a)] (instance? java.lang.ClassCastException exception) (.contains (.getMessage exception) "Symbol cannot be cast to java.lang.Number")) true user =>
When viewing the specdoc, the spec is also verified at this time to ensure that the spec you are reading actually conforms to what the function does.
Let bindings on a spec
To avoid duplication, it is sometimes useful to use variables inside your specs.
(defspec + [a 'a exp-exception (try (+ 1 a) (catch Exception e e))] ([1 a] (= (.getMessage exception) (.getMessage exp-exception)))) user=> (specdoc +) ------------------------- clojure/+ ( [x] [x y] [x y & more]) Returns the sum of nums. (+) returns 0. Spec: [a (quote a) exp-exception (try (+ 1 a) (catch Exception e e))] ([1 a] (= (.getMessage exception) (.getMessage exp-exception))) true
One more example
Just a silly little function with a spec to match it.
(ns user (:use clojure.spec)) (defn my-test-fn ( (my-test-fn 'a)) ([arg] (cond (= arg 'a) (println "You passed in 'a") (= arg 'b) (throw (new Exception "You passed in 'b")) (= arg 'c) (do (println "You passed in 'c") 'c) :else arg))) (defspec my-test-fn [arg-number-error-message "Wrong number of args passed to: my-test-fn"] ( (= "You passed in 'a\n" sysout)) (['a] (= "You passed in 'a\n" sysout)) (['b] (= "You passed in 'b" (.getMessage exception))) (['c] (= "You passed in 'c\n" sysout) (= value 'c)) (['d] (= value 'd)) ( (= value 2)) (['d 3] (instance? java.lang.IllegalArgumentException exception) (.contains (.getMessage exception) arg-number-error-message)))