如何在测试套件中包含 clojure.spec' 函数

Howto include clojure.spec'd functions in a test suite

是否可以在通用测试套件中包含 clojure.spec 的函数?我知道我们可以 register specs and directly spec functions.

(ns foo
  (:require [clojure.spec :as s]
            [clojure.spec.test :as stest]))

(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?)

之后,如果我想 运行 针对该规范函数进行生成测试,我可以使用 stest/check

=> (stest/check `average)
({:spec #object[clojure.spec$fspec_impl$reify__14282 0x68e9f37c "clojure.spec$fspec_impl$reify__14282@68e9f37c"], :clojure.spec.test.check/ret {:result true, :num-tests 1000, :seed 1479587517232}, :sym edgar.core.analysis.lagging/average})

但是 i) 是否可以在我的通用测试套件中包含这些测试 运行?我正在考虑 clojure.test 集成 test.check has. The closest thing that I can see ii) is the stest/instrument (see here) 功能。但这似乎只是让我们打开对 repl 的检查。不完全是我想要的。此外,iii) 是否注册了功能规范?

(defspec foo-test 
         100 

         ;; NOT this
         #_(prop/for-all [v ...]
           (= v ...))

         ;; but THIS
         (stest/some-unknown-spec-fn foo))

好的,解决了这个问题。事实证明,没有开箱即用的解决方案。但是 clojure-spec slack 频道上的一些人已经为 clojure.spec.testclojure.test[=30 整理了一个 defspec-test 解决方案=].

所以给出问题中的代码。您可以 A) 定义 defspec-test 宏,该宏采用您的测试名称和规范函数列表。然后您可以 B) 在您的测试套件中使用它。

感谢 Clojure 社区!!希望这样的实用函数能够将其纳入核心库。

A)

(ns foo.test
  (:require [clojure.test :as t]
            [clojure.string :as str]))

(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#)]]
                                      (t/do-report
                                        {:type     :fail
                                         :message  (with-out-str (clojure.spec/explain-out failure#))
                                         :expected (->> r# :spec rest (apply hash-map) :ret)
                                         :actual   (if (instance? Throwable failure#)
                                                     failure#
                                                     (:clojure.spec.test/val failure#))})))
                                  checks-passed?#)))
        (fn [] (t/test-var (var ~name)))))))

B)

(ns foo-test
  (:require [foo.test :refer [defspec-test]]
            [foo]))


(defspec-test test-average [foo/average])

以上 can fail in the case where :failure is false due to how stest/abbrev-result tests for failure. See CLJ-2246了解更多详情。您可以通过定义您自己的 abbrev-result 版本来解决这个问题。此外,故障数据的格式也发生了变化。

(require
 '[clojure.string :as str]
 '[clojure.test :as test]
 '[clojure.spec.alpha :as s]
 '[clojure.spec.test.alpha :as stest])

;; extracted from clojure.spec.test.alpha
(defn failure-type [x] (::s/failure (ex-data x)))
(defn unwrap-failure [x] (if (failure-type x) (ex-data x) x))
(defn failure? [{:keys [:failure]}] (not (or (true? failure) (nil? failure))))

;; modified from clojure.spec.test.alpha
(defn abbrev-result [x]
  (let [failure (:failure x)]
    (if (failure? x)
      (-> (dissoc x ::stc/ret)
          (update :spec s/describe)
          (update :failure unwrap-failure))
      (dissoc x :spec ::stc/ret))))

(defn throwable? [x]
  (instance? Throwable x))

(defn failure-report [failure]
  (let [expected (->> (abbrev-result failure) :spec rest (apply hash-map) :ret)]
    (if (throwable? failure)
      {:type :error
       :message "Exception thrown in check"
       :expected expected
       :actual failure}
      (let [data (ex-data (get-in failure
                                  [::stc/ret
                                   :result-data
                                   :clojure.test.check.properties/error]))]
        {:type     :fail
         :message  (with-out-str (s/explain-out data))
         :expected expected
         :actual   (::s/value data)}))))

(defn check?
  [msg [_ body :as form]]
  `(let [results# ~body
         failures# (filter failure? results#)]
     (if (empty? failures#)
       [{:type    :pass
         :message (str "Generative tests pass for "
                       (str/join ", " (map :sym results#)))}]
       (map failure-report failures#))))

(defmethod test/assert-expr 'check?
  [msg form]
  `(dorun (map test/do-report ~(check? msg form))))

这是 grzm 的出色答案的略微修改版本,适用于 [org.clojure/test.check "0.10.0-alpha4"]。它使用来自此 PR 的新 :pass? 密钥:https://github.com/clojure/test.check/commit/09927b64a60c8bfbffe2e4a88d76ee4046eef1bc#diff-5eb045ad9cf20dd057f8344a877abd89R1184.

(:require [clojure.test :as t]
          [clojure.string :as str]
          [clojure.spec.alpha :as s]
          [clojure.spec.test.alpha :as stest])

(alias 'stc 'clojure.spec.test.check)

;; extracted from clojure.spec.test.alpha
(defn failure-type [x] (::s/failure (ex-data x)))
(defn unwrap-failure [x] (if (failure-type x) (ex-data x) x))

;; modified from clojure.spec.test.alpha
(defn abbrev-result [x]
  (if (-> x :stc/ret :pass?)
    (dissoc x :spec ::stc/ret)
    (-> (dissoc x ::stc/ret)
        (update :spec s/describe)
        (update :failure unwrap-failure))))

(defn throwable? [x]
  (instance? Throwable x))

(defn failure-report [failure]
  (let [abbrev (abbrev-result failure)
        expected (->> abbrev :spec rest (apply hash-map) :ret)
        reason (:failure abbrev)]
    (if (throwable? reason)
      {:type :error
       :message "Exception thrown in check"
       :expected expected
       :actual reason}
      (let [data (ex-data (get-in failure
                                  [::stc/ret
                                   :shrunk
                                   :result-data
                                   :clojure.test.check.properties/error]))]
        {:type     :fail
         :message  (with-out-str (s/explain-out data))
         :expected expected
         :actual   (::s/value data)}))))

(defn check?
  [msg [_ body :as form]]
  `(let [results# ~body
         failures# (remove (comp :pass? ::stc/ret) results#)]
     (if (empty? failures#)
       [{:type    :pass
         :message (str "Generative tests pass for "
                       (str/join ", " (map :sym results#)))}]
       (map failure-report failures#))))

(defmethod t/assert-expr 'check?
  [msg form]
  `(dorun (map t/do-report ~(check? msg form))))

用法:

(deftest whatever-test
  (is (check? (stest/check `whatever
                            ;; optional
                            {:clojure.spec.test.check/opts {:num-tests 10000}})))