A Gotcha: `test.core/thrown?`

Clojure is generally such a pristine and sensible language, it took me some debugging time to find out why this was failing:

  (is
   (let [proctorless-rmap {:tester (:id tester)
			   :test (:id this-test)}]
     (thrown? Exception (xreg/register-for-exam proctorless-rmap)) "no proctor should fail"))
;; Unable to resolve symbol: thrown? in this context

Trying to solve this at the repl, I was stumped for quite a while by why I couldn’t locate the thrown? function with C-c C-. to locate function definitions. I checked my Clojure version, my syntax, and FINALLY the core documentation on clojure.test/is, which revealed that “thrown? is a special form” – hence the reason I couldn’t locate it as a function or macro itself.

On the plus side, I was able to use for the first time the strict parens function sp-convolute-sexp to rewrite this sensibly in one keystroke (after putting my cursor after the let’s vector bracket):

(testing "Registration"
    (let [proctorless-rmap {:tester (:id tester)
			    :test (:id this-test)}]
      (is (thrown? Exception (xreg/register-for-exam proctorless-rmap)) "no proctor should fail"))
    (is false "with proctor should pass")
    (is false "Should be an exam registered now"))

This is one more reason I generally dislike macros (yeah, I know that makes me a Lisp infidel…): they can break the syntactic consistency and tooling that go so beautifully with Lisp and be far harder to debug than Clojure usually is. At least I got to convolute, so it wasn’t a COMPLETE waste of 30 minutes.

1 Like

It’s probably worth noting that (is (= expected actual)) relies on = as a special symbol too – it is not clojure.core/=. clojure.test lets you extend the syntax inside an is with multi-methods. expectations.clojure.test takes advantage of this to define meaning for =? which is a more permissive assertion test that allows the “expected” expression to be a value (for traditional equality), a predicate, or a spec. And then it wraps is in another macro so you can just say things like:

(expect even? some-value)
(expect ::my-spec some-value)
(expect 42 some-value)

which will expand to the appropriate calls to is with =? which in turn dispatches to (even? some-value), (s/valid? ::my-spec some-value), and (= 42 some-value) in a smart way.

Expectations lets you write things like (expect 42 (in some-value)) (checking if the set/sequence some-value contains the value 42) or (expect {:a 42} (in some-value)) (checking if the map some-value contains the sub-map with key :a and value 42). Plus a whole bunch of other very convenient stuff.

5 Likes

Whoa! Nice elaboration on these special cases. I’m definitely looking into expectations too.

You may be happier if you take a look at my Clojure template project:

It has a much nicer testing interface that wraps the basic clojure.test but is simpler & easier to use. A sample:

(ns tst.demo.core
  (:use demo.core tupelo.core tupelo.test)
  (:require
    [tupelo.string :as ts])
  (:import [demo Calc]))

(dotest
  (is= 5 (+ 2 3))
  (isnt= 9 (+ 2 3))
  (throws? (/ 1 0)) ; verify that an illegal operation throws an exception

  (is= 3 (add2 1 2))
  (throws? (add2 1 "two"))) ; Prismatic Schema will throw since "two" is not a number


(dotest   ; Java source code testing
  (isnt= 5 (Calc/add2 4 1)) ; returns a double, so this fails
  (is= 5.0 (Calc/add2 4 1)) ; correct version
  (is (rel= 5.000000001
        (Calc/add2 4 1) :digits 5))) ; another option
```
1 Like

How does it generate repeatable test names so that regular clojure.test tooling works properly with it? That was one of the big problems with Expectations and why I created a new clojure.test-compatible version that has named tests.

The (dotest ...) macro uses the line number to generate a unique symbol name like dotest-line-xxxx. This was the original motivation to create tupelo.test. Previously, I would cut/past an existing test to, then modify it to create a new test. Several times I forgot to change the test name, which shadowed the previous test.

I also found it irritating being forced to come up with test names, since it was usually either obvious what was being tested or unimportant to give it a unique name. It is so much easier to simply say (dotest ...) and keep moving. If a test needs explanation, a simple comment (or many!) works wonderfully.

Alan

The danger there is that you change one of the tests, and change the line numbers of other tests, and then you get dotest-line-42 and dotest-line-43 etc and you end up with a dirty namespace.

I never run tests from the repl. I use either a simple lein test or lein test-refresh.

I think the lein-test-refresh plugin is one of the biggest aids to Clojure productivity ever. the Koacha library has similar capabilities.

https://github.com/jakemcc/lein-test-refresh

https://github.com/lambdaisland/kaocha

Alan

My workflow is “RDD” so it’s a very tight edit/eval cycle and that includes running tests. I only run tests from the command-line when I’m running some subset of our entire suite as a sanity check prior to commits. I never type into a REPL – I edit code and eval it, often without even saving, while I’m developing code and developing tests. I can run an individual test in my editor with a keystroke, or an entire namespace full of tests. I’ve experimented with a lot of different workflows over the nearly ten years I’ve been doing Clojure and this is the most productive I’ve ever been.

But, yeah, if you’re only running tests from the command-line, I can see how you wouldn’t trip over the REPL getting “dirty” from constantly renaming tests as you work.

The test-refresh plugin automatically re-loads and re-runs the tests in any ns upon save from the editor. You can use “focus” metadata to limit the run to individual namespaces and/or functions. So, although I keep it running in an adjacent terminal window, I never need to leave the editor. Just type :wa (Vim plugin to IntelliJ) and the tests automatically re-run, usually in about 0.1 seconds.

I love Cider’s auto-test-mode for this same reason, rerunning the appropriate tests whenever you re-evaluate a buffer. Also invaluable is the function to rerun whichever tests failed from your last run.

Yes, I use Koacha’s repeating test capability and it’s WONDERFUL. Definitely a great productivity enhancer, particularly when refactoring. Just move code around, save, and look for the test output to go green, and keep moving. If it goes red, dive in and figure out what went wrong.

1 Like

Can anyone tell me how Kaocha’s repeating testing is different from Cider’s auto-testing, or are they the same?