Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
clojure.spec.test integration with clojure.test
(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)))))))
@carocad

This comment has been minimized.

Copy link

@carocad carocad commented Jan 6, 2017

for the record, the way to use it is the same as you would use clojure.spec.test/check
for example:

(stest/check `fully.qualified.function/name {:clojure.spec.test.check/opts {:num-tests 50}})
@carocad

This comment has been minimized.

Copy link

@carocad carocad commented Jan 6, 2017

In case you get a weird exception when running lein test check if it is something like this: technomancy/leiningen#2173 . It might save you some frustration ;)

@alexandergunnarson

This comment has been minimized.

Copy link

@alexandergunnarson alexandergunnarson commented May 10, 2018

Thanks for the gist @kennyjwilli — very helpful! For what it's worth, here is an ostensibly CLJS-safe (untested in CLJS, but tested in CLJ) code snippet based on yours that 1) generates much less code in defspec-test and 2) includes all requires necessary to make it work:

(ns put-this-wherever
  (:require
    [clojure.spec.alpha      :as s]
    [clojure.spec.test.alpha :as stest]
    [clojure.string          :as str]
    [clojure.test            :as test]))

(defn report-results [check-results]
  (let [checks-passed? (->> check-results (map :failure) (every? nil?))]
    (if checks-passed?
        (test/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       (stest/abbrev-result failed-check)
                failure (:failure r)]
            (test/do-report
              {:type     :fail
               :message  (with-out-str (s/explain-out failure))
               :expected (->> r :spec rest (apply hash-map) :ret)
               :actual   (if (instance? #?(:clj Throwable :cljs js/Error) failure)
                             failure
                             (::stest/val failure))}))))
    checks-passed?))

#?(:clj
(defmacro defspec-test
  ([name sym-or-syms] `(defspec-test ~name ~sym-or-syms nil))
  ([name sym-or-syms opts]
   (when test/*load-tests*
     `(defn ~(vary-meta name assoc :test
              `(fn [] (report-results (stest/check ~sym-or-syms ~opts))))
        [] (test/test-var (var ~name)))))))
@arichiardi

This comment has been minimized.

Copy link

@arichiardi arichiardi commented Aug 18, 2018

Exactly what I was looking for! Is there any lib that include this bit of code?

@bhb

This comment has been minimized.

Copy link

@bhb bhb commented Aug 18, 2018

Nice work! Here is a very small change that prints the check result with Expound

(ns put-this-wherever
  (:require
    [clojure.spec.alpha      :as s]
    [clojure.spec.test.alpha :as stest]
    [clojure.string          :as str]
    [clojure.test            :as test]
    [expound.alpha           :as expound]))

(defn report-results [check-results]
  (let [checks-passed? (->> check-results (map :failure) (every? nil?))]
    (if checks-passed?
      (test/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       (stest/abbrev-result failed-check)
              failure (:failure r)]
          (test/do-report
           {:type     :fail
            :message  (binding [s/*explain-out* (expound/custom-printer {:theme :figwheel-theme})]
                        (expound/explain-results-str check-results))
            :expected (->> r :spec rest (apply hash-map) :ret)
            :actual   (if (instance? #?(:clj Throwable :cljs js/Error) failure)
                        failure
                        (::stest/val failure))}))))
    checks-passed?))

#?(:clj
   (defmacro defspec-test
     ([name sym-or-syms] `(defspec-test ~name ~sym-or-syms nil))
     ([name sym-or-syms opts]
      (when test/*load-tests*
        `(defn ~(vary-meta name assoc :test
                           `(fn [] (report-results (stest/check ~sym-or-syms ~opts))))
           [] (test/test-var (var ~name)))))))

For the following test:

(s/fdef foo
        :args (s/cat :x int? :y int?)
        :ret pos-int?)
(defn foo [x y]
  (+ x y))

(defspec-test foo-test `foo)

The normal output is:

val: 0 fails at: [:ret] predicate: pos-int?

expected: pos-int?
  actual: 0

whereas the output with Expound is:

== Checked expound.problems-test/foo ========

-- Function spec failed -----------

  (expound.problems-test/foo 0 0)

returned an invalid value.

  0

should satisfy

  pos-int?

-------------------------
Detected 1 error

expected: pos-int?
  actual: 0
@WhittlesJr

This comment has been minimized.

Copy link

@WhittlesJr WhittlesJr commented Apr 29, 2019

This is fabulous and really should be in a library

@WhittlesJr

This comment has been minimized.

Copy link

@WhittlesJr WhittlesJr commented May 31, 2019

Guess what, it's about to be lambdaisland/kaocha#95

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.