In my code we often throw our own exceptions with
(throw (ex-info "Custom exception" {:cause "reasons" :data {}}))
When this hits our tests though,is there a way to meaningfully reason about its :data or :causes or something else? The usual (is (thrown? Exception ...))
doesn’t seem to foot the bill.
1 Like
This is one of the reasons I like expectations.clojure.test
– Expectations-style syntax but clojure.test
compatible: https://github.com/clojure-expectations/clojure-test/blob/develop/doc/more.md#expect-more-
The example given is for using a regex on the thrown exception message:
(is (thrown-with-msg? ArithmeticException #"Divide by zero" (/ 1 0)))
(expect (more-> ArithmeticException type
#"Divide by zero" ex-message)
(/ 1 0))
but you could add a clause for ex-data
in there and “expect” whatever predicates you want over that, or thread through it:
(expect (more-> Exception type
#"reasons" (-> (ex-message) :cause))
(some-expr))
You can use this Expectations library together with clojure.test
, so the above could be in a deftest
.
3 Likes
Note that thrown?
and thrown-with-msg?
are two default implementations of the clojure.test/assert-expr
multimethod. You can make an implementation for handling clojure.lang.ExceptionInfo
or the CLJS equivalent. This is a straightforward implementation that compares the data, but you probably want to make this check for sub-maps (does the data you expect occur in the data returned).
(require '[clojure.test :as t :refer [deftest is do-report]])
(defmethod t/assert-expr 'thrown-with-data? [msg form]
(let [data (second form)
body (nthnext form 2)]
`(try ~@body
(do-report {:type :fail, :message ~msg,
:expected '~form, :actual nil})
(catch clojure.lang.ExceptionInfo e#
(let [expected# ~data
actual# (ex-data e#)]
(if (= expected# actual#)
(do-report {:type :pass, :message ~msg,
:expected expected#, :actual actual#})
(do-report {:type :fail, :message ~msg,
:expected expected#, :actual actual#})))
e#))))
(deftest foo
(is (thrown-with-data? {:a 1} (throw (ex-info "" {:a 1}))))
(is (thrown-with-data? {:b 1} (throw (ex-info "" {:a 1})))))
(t/run-tests)
$ clj /tmp/test.clj
Testing user
FAIL in (foo) (test.clj:24)
expected: {:b 1}
actual: {:a 1}
Ran 1 tests containing 2 assertions.
1 failures, 0 errors.
Bonus, also works in babashka:
$ bb /tmp/test.clj
Testing user
FAIL in (foo) (/private/tmp/test.clj:22)
expected: {:b 1}
actual: {:a 1}
Ran 1 tests containing 2 assertions.
1 failures, 0 errors.
{:test 1, :pass 1, :fail 1, :error 0, :type :summary}
14 Likes
One thing to remember if you do this is that there is no namespacing for these multimethods. When we did this at a previous company I prefixed the multimethod with the company name to the assertion to avoid collisions, e.g. acme-thrown-with-data?
.
1 Like
alidlo
November 18, 2020, 2:50am
5
Another alternative to extending the t/assert-expr
multi-method, as shown by @borkdude , is to write a macro that catches the thrown ex-info and returns its data.
Just wrote this for a library I’m testing:
(defmacro catch-thrown-info [f]
`(try
~f
(catch
clojure.lang.ExceptionInfo e#
{:msg (ex-message e#) :data (ex-data e#)})))
Sample usage:
(is (= (catch-thrown-info (throw (ex-info "Error" {:err 1})))
{:msg "Error" :data {:err 1}}))
2 Likes
alidlo
November 18, 2020, 3:02am
6
Ah, now seeing that there’s actually a thrown-with-msg?
helper now in clojure.test
(defmethod assert-expr 'thrown-with-msg? [msg form]
;; (is (thrown-with-msg? c re expr))
;; Asserts that evaluating expr throws an exception of class c.
;; Also asserts that the message string of the exception matches
;; (with re-find) the regular expression re.
(let [klass (nth form 1)
re (nth form 2)
body (nthnext form 3)]
`(try ~@body
(do-report {:type :fail, :message ~msg, :expected '~form, :actual nil})
(catch ~klass e#
(let [m# (.getMessage e#)]
(if (re-find ~re m#)
(do-report {:type :pass, :message ~msg,
:expected '~form, :actual e#})
(do-report {:type :fail, :message ~msg,
:expected '~form, :actual e#})))
e#))))
system
Closed
May 19, 2021, 3:02pm
7
This topic was automatically closed 182 days after the last reply. New replies are no longer allowed.