Anatomy of a test

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)))))))

    Tip

    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:

Anatomy of a test

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:

  • Keep our tests focused
  • Produce output that makes a bit more sense

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:

Anatomy of a test

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.69.199