Better exception output for test.check

Here’s a hack for getting better output when test.check tests run via clojure.test throw exceptions, thanks to Dominic Monroe.

TL;DR

If you require the following code somewhere in your test suite, then you’ll still get shown the usual helpful test.check output - in particular, the :seed - if the code under test throws an exception:

(alter-var-root
 #'clojure.test.check.clojure-test/assert-check
 (constantly
  (fn [{:keys [result result-data] :as m}]
    (if-let [error (:clojure.test.check.properties/error result-data)]
      (clojure.test/do-report
       {:type :error
        :message (-> m
                     (dissoc :result :result-data)
                     (update :shrunk dissoc :result)
                     (update-in [:shrunk :result-data]
                                dissoc
                                :clojure.test.check.properties/error))
        :expected {:result true}
        :actual error})
      (clojure.test/is (clojure.test.check.clojure-test/check? m))))))

What problem does this fix?

If we run the following test.check test using clojure.test (via the defspec macro, which provides integration between the two)…

(require '[clojure.test.check.generators :as gen])
(require '[clojure.test.check.properties :as prop])
(require '[clojure.test.check.clojure-test :refer [defspec]])

(defn broken-identity [x]
  (if (= x 42) 0 x))

(defspec broken-identity-should-return-argument
  (prop/for-all
   [x gen/small-integer]
   (= x (broken-identity x))))

then we get the following useful output from test.check:

{:fail [42]
 :failed-after-ms 5
 :failing-size 85
 :num-tests 86
 :pass? false
 :result false
 :result-data nil
 :seed 1696006147623
 :shrunk {:depth 0
          :pass? false
          :result false
          :result-data nil
          :smallest [42]
          :time-shrinking-ms 0
          :total-nodes-visited 6}
 :test-var "broken-identity-should-return-argument"}

In particular, this includes the seed. This effectively lets us replay the test case if we wanted:

(require '[clojure.test.check :as tc])
(tc/quick-check
 100
 (prop/for-all
  [x gen/small-integer]
  (= x (broken-identity x)))
 {:seed 1696006147623})

However - if the bug with the code under test throws an exception, then sadly we get a fraction of the data we would ordinarily get:

(defn exploding-identity [x]
  (if (= x 42)
    (throw (Exception. "Boom!"))
    x))

(defspec exploding-identity-should-return-argument 200
  (prop/for-all
   [x gen/small-integer]
   (= x (exploding-identity x))))
;; Test output
{:clojure.test.check.clojure-test/params [42]
 :clojure.test.check.clojure-test/property
   #clojure.test.check.generators.Generator
    {:gen #object [clojure.test.check.generators$gen_fmap$fn__2247 0x2801827a
                   "clojure.test.check.generators$gen_fmap$fn__2247@2801827a"]}
 :type :clojure.test.check.clojure-test/shrunk}

We still get given the data that causes the problem, so that’s good. But it would still be nice to get the seed since that would be easier to copy-paste out of a large test report when working with much more complicated test data.

However, if we require Dominic’s code shown above, we can get some better test output again:

(alter-var-root
 #'clojure.test.check.clojure-test/assert-check
 ;;; etc
 )
;; Test output
{:fail [42]
 :failed-after-ms 6
 :failing-size 95
 :num-tests 96
 :pass? false
 :seed 1696007827585
 :shrunk {:depth 0
          :pass? false
          :result-data {}
          :smallest [42]
          :time-shrinking-ms 1
          :total-nodes-visited 6}
 :test-var exploding-identity-should-return-argument}

That’s better!

Also, test runners play a bit more nicely with it; in Cider, without the fix I just get the following output:

Error in exploding-identity-should-return-argument
Uncaught exception, not in assertion
   error: java.lang.Exception: Boom!

I can’t even see the problematic input, never mind the seed!

But with the fix in place, I get:

Error in exploding-identity-should-return-argument
{:shrunk {:total-nodes-visited 6, :depth 0, :pass? false, :result-data {},
          :time-shrinking-ms 0, :smallest [42]}
 :failed-after-ms 4, :num-tests 48, :seed 1696008029883,
 :fail [42], :failing-size 47,
 :pass? false,
 :test-var "exploding-identity-should-return-argument"}
 expected: {:result true}
   error: java.lang.Exception: Boom!

Nice!

It’s a shame that the Jira for this issue isn’t getting any attention - but at least there’s a workaround we can make use of.