There are three macros in the clojure.test
namespace, which are the trifecta of testing in Clojure: deftest
, is
, and testing
. These three macros are explained as follows:
deftest
: This is similar to def
or defn
and defines our test function. Tests may call other tests, however, this is a practice that I consider dangerous, as it can result in fragile tests that are, by their nature, rather difficult to debug.Usage: (deftest name & body
)
is
: This is used to make an assertion in our test. The predicate we pass to is
should return a Boolean. We can also optionally provide a message, which will be attached to the assertion and displayed as part of the failure written to stdout
.Usage: (is form
)
(is form message
)
is
also allows the following two special forms that can be used to check for exceptions:
thrown?
: This is used to ensure that an exception of a specific type is thrown, and if not, fails the test.Usage: (thrown? e form
)
Here's an example of this form:
(is (thrown? InvocationTargetException (an-unsound-function)))
thrown-with-msg?
: This does the same as thrown
, but additionally asserts that the exception's message matches a regular expression.Usage: (thrown-with-msg? e regex form
)
Here's an example of this form:
(is (thrown-with-msg? InvocationTargetException #"StackOverflowError" (an-unsound-function)))
testing
: This allows us to provide an additional context to the list of testing contexts within a test function. Testing contexts can be nested, but the root testing context must reside inside deftest
.Usage: (testing string & body)
Here's an example of this form:
(deftest some-cool-tests (testing "a testing context:" (testing "a nested testing context" (let [some-val 6] (is (some #{some-val} (range 1 10)))))))
Note that the clojure.test
namespace is by no means limited to just these three macros. For a comprehensive list of functionalities of clojure.test
, refer to the docs at https://clojure.github.io/clojure/clojure.test-api.html.
While the preceding macros allow you to create a range of tests and save you some typing, ultimately, I think they produce poor output. When a test fails, we don't want to have to think "Huh, I wonder what went wrong?" We should, ideally, know exactly what went wrong. If we change the value of some-val
from 6
to 10
, the test will fail, producing the following output:
This is somewhat useful. However, I prefer each test to assert something specific, and for the test name to be a statement of fact. There's nothing worse than seeing a failing test where the output message is FAIL in (test-service)
. What failed in the service? What were we actually testing? The "statement-of-fact" as a test name tends to:
Let's change our preceding example test to not use any testing contexts, and instead use a simple deftest
with a statement of fact, but keep the value at 10
so that the test fails:
(deftest test-value-must-fall-between-1-and-9 (let [some-val 10] (is (some #{some-val} (range 1 10)))))
This test will now produce the following output when it fails:
Of course, you can name and write your tests however you want, I do not judge. I've found in my experience, however, that the "statement-of-fact-test-name" convention, despite creating lengthy test names, tends to produce focused tests, resulting in less confusion and frustration when a test inevitably fails.
3.16.69.199