суббота, 1 апреля 2017 г.

clojure.test with clojure.spec

First of all, add necessary dependency to project.clj
  [org.clojure/test.check "0.9.0"]

Then add following line to project.clj  :monkeypatch-clojure-test false due to bug https://github.com/technomancy/leiningen/issues/2173

Then, define fns and their spec

(require '[clojure.spec :as s])

(defn average [list-sum list-count]
  (/ list-sum list-count))

(s/fdef average
  :args (s/and (s/cat :list-sum float? :list-count integer?)
               #(not (zero? (:list-count %))))
  :ret number?)

Then in test folder make file with tests and put

(:require [clojure.test :as t]
            [clojure.string :as str]
            [example.core :refer :all]
            [clojure.spec :as spec]
            [clojure.pprint :as pprint]
            [clojure.spec.test :as stest])

(defmacro defspec-test
  ([name sym-or-syms] `(defspec-test ~name ~sym-or-syms nil))
  ([name sym-or-syms opts]
   (when t/*load-tests*
     `(def ~(vary-meta name assoc :test `(fn []
                                           (let [check-results# (clojure.spec.test/check ~sym-or-syms ~opts)
                                                 checks-passed?# (every? nil? (map :failure check-results#))]
                                             (if checks-passed?#
                                               (t/do-report {:type    :pass
                                                             :message (str "Generative tests pass for "
                                                                           (str/join ", " (map :sym check-results#)))})
                                               (doseq [failed-check# (filter :failure check-results#)
                                                       :let [r# (clojure.spec.test/abbrev-result failed-check#)
                                                             failure# (:failure r#)]]
                                                   {:type     :fail
                                                    :message  (with-out-str (clojure.spec/explain-out failure#))
                                                    :expected (->> r# :spec rest (apply hash-map) :ret)
                                                    :actual   (if (instance? Throwable failure#)
                                                                (:clojure.spec.test/val failure#))})))
        (fn [] (t/test-var (var ~name)))))))

(defspec-test average-test ['example.core/average] {:clojure.spec.test.check/opts {:num-tests 500000}})

Notice that testing fn is fully qualified and ' at the begining in vector.

That's it.


Also, we can write usual test. To test function example.core/average with input auto generation and output validation with spec we can do it followin way:

(t/deftest average-test2
  (t/testing "average test"  
    (let [r (stest/check `example.core/average {:clojure.spec.test.check/opts {:num-tests 1000}})]
        (t/is (= true (-> r first :clojure.spec.test.check/ret :result))))))

Update 2:

To test function example.core/average we need to require [clojure.test.check.clojure-test :as check.clj-test :refer [defspec]]

(defspec average-test3   10   (prop/for-all [r (gen/tuple gen/int gen/int)]
                                                    (number? (apply example.core/average r))))

Update 3:
To run reproducible test we need to require [clojure.test.check.clojure-test :as check.clj-test :refer [defspec]]

(defspec abcd {:seed 12345 :num-tests 1000}
         (prop/for-all [s (s/gen ::basic/blank-string)]
                       (s/valid? ::basic/blank-string s)))

Update 4:

In example.core namespace:

(defn my-div [x y]
  "divide two numbers"
  (/ x y))

(s/fdef my-div
  :args (s/and (s/cat :first number? :second number?)
               #(not (zero? (:second %))))
  :ret number?)

In tests namespace:

(deftest my-div-test
  (let [result (spec.test/check `example.core/my-div {:clojure.spec.test.check/opts {:num-tests 1000}})]
    (is (= true (-> result first :clojure.spec.test.check/ret :result)))
    (clojure.spec.test/instrument 'example.core/my-div)
    (is (thrown? clojure.lang.ExceptionInfo (example.core/my-div 1 0)))
    (clojure.spec.test/unstrument example.core/my-div)
    (is (thrown? ArithmeticException (example.core/my-div 1 0)))))

Комментариев нет:

Отправить комментарий