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