23 February 2022

Table-driven Tests in Clojure

There's a macro for that...

On a whim I decided to dust off the prime factors kata. Here's the production code I usually end up with:

(defn prime-factors [n]
  (loop [n n, div 2, factors []]
    (cond (= n 1) factors
          (zero? (mod n div)) (recur (/ n div) div (conj factors div))
          :else (recur n (inc div) factors))))

..but that's not what I want to talk about. I'd like to talk about the tests. I've used speclj as the test library before--but this time I wanted to use the built-in clojure.test namespace.

(deftest test-prime-factors-with-is
  (is (= [] (prime-factors 1)))
  (is (= [2] (prime-factors 2)))
  (is (= [3] (prime-factors 3)))
  (is (= [2 2] (prime-factors 4)))
  (is (= [5] (prime-factors 5)))
  (is (= [2 3] (prime-factors 6)))
  (is (= [7] (prime-factors 7)))
  (is (= [2 2 2] (prime-factors 8)))
  (is (= [3 3] (prime-factors 9)))
  (is (= [2 5] (prime-factors 10))))

Compared with statically typed languages (java, go, etc...) the above code is quite concise. But there's still a lot of repetition in those assertions. How about we do something like a list comprehension?

(deftest test-prime-factors-with-doseq
  (doseq [[input expected] [[1 []]
                            [2 [2]]
                            [3 [3]]
                            [4 [2 2]]
                            [5 [5]]
                            [6 [2 3]]
                            [7 [7]]
                            [8 [2 2 2]]
                            [9 [3 3]]
                            [10 [2 5]]]]
    (is (= expected (prime-factors input)))))

Less repetition, but it looks a big clunky. But there's a nice plural version of is: are

The are macro allows you to define a concise, table-driven test suite:

(deftest test-prime-factors-with-are
  (are [input expected]
    (= expected (prime-factors input))
    1 []
    2 [2]
    3 [3]
    4 [2 2]
    5 [5]
    6 [2 3]
    7 [7]
    8 [2 2 2]
    9 [3 3]
    10 [2 5]))

Isn't that nice? But, what's going on? Here are some inline comments that add some explanation:

(deftest test-prime-factors-with-are
  (are
    ; The following vector defines the arguments common to each test:
    [input expected]

    ; This form is the assertion 'template' for the arguments:
    (= expected (prime-factors input))

    ; Here are the test cases, arranged in groups of template arguments
    1 []
    2 [2]
    3 [3]
    4 [2 2]
    5 [5]
    6 [2 3]
    7 [7]
    8 [2 2 2]
    9 [3 3]
    10 [2 5]))