Clojure bites - clojure.test/are

by FPSD — Tue 02 May 2023


Overview

Clojure's clojure.test/are provides a data driven approach to unit testing. Lets start with a practical example, implementing the most important function in the history of computer science, FizzBuzz! (But only because all of my binary search trees are already balanced).

FizzBuzz

It is (was?) common that, during an interview, to be asked to implement the logic of the FizzBuzz game, Wikipedia has a nice article about it.

It can be summarized as follows:

Write a function that takes a numerical argument and returns:

The implementation

Lets start by defining the test suite using the usual clojure.test/is macro.

    (ns fizzbuzz.core-test
      (:require [clojure.test :refer [deftest is testing]]
                [fizzbuzz.core :as sut]))
    
    (deftest OMG-FizzBuzz
      (testing "Should return the numerical argument"
        (is (= 1 (sut/fizz-buzz 1))))
    
      (testing "Should return Fizz"
        (is (= "Fizz" (sut/fizz-buzz 3))))
      (testing "Should return Buzz"
        (is (= "Buzz" (sut/fizz-buzz 5))))
      (testing "Should return FizzBuzz"
        (is (= "FizzBuzz" (sut/fizz-buzz 15))))
      )

Please note that sut stands for system under test; I've seen it being used here and there but I am not sure it is a best practice or not.

The test will clearly fail because there is no fizz-buzz function or even a fizzbuzz.core namespace. Lets start with a trivial implementation.

    (ns fizzbuzz.core)
    
    (defn fizz-buzz [n]
      (cond
        (= 0 (mod n 15)) "FizzBuzz"
        (= 0 (mod n 3)) "Fizz"
        (= 0 (mod n 5)) "Buzz"
        :else n))

Now all tests are passing, the interviewer is more than happy but you want to show off your skills and ask to improve both code and tests

Improvements

First thing to notice is that if a number is not a multiple of 3 or 5 then we run 4 divisions and return n. A slightly improvement can be the following:

    (ns fizzbuzz.core)
    
    (defn fizz-buzz
      [n]
      (cond
        (= 0 (mod n 3)) (if (= 0 (mod n 5)) "FizzBuzz" "Fizz")
        (= 0 (mod n 5)) "Buzz"
        :else n))

Test are passing so we are confident that the function is working as expected, and it is a bit more performing! Yes, we are not solving the world's energy crisis but it is something.

Data driven tests

Looking at the tests we can notice that we are calling the same function with different input values and expecting a specific result. User of other testing libraries, for example Pytest may be familiar with the parametrize decorator that takes tuples of data and calls the test case with that data as parameters. In Clojure we can achieve that with clojure.test/are macro, here is the docstring:

"Checks multiple assertions with a template expression. See clojure.template/do-template for an explanation of templates."

A bit cryptic but an example can help us understand better.

    (ns fizzbuzz.core-test
      (:require [clojure.test :refer [deftest are]]
                [fizzbuzz.core :as sut]))
    
    (deftest OMG-FizzBuzz
      (are [argument expected] (= expected (sut/fizz-buzz argument))
        1 1
        2 2
        3 "Fizz"
        6 "Fizz"
        5 "Buzz"
        10 "Buzz"
        15 "FizzBuzz"))

And voila, we have a data driven test suite for our implementation!

Is it all raibows and unicorns?

clojure.test/are comes with few shortcomings:

Alternatives

clojure.test/are is a builtin macro but it comes with some problems (not so big IMHO), so are there valuable alternatives? Some lovely people pointed them out in the comments of the post on Reddit:

Closing words

I hope this will encourage exploring Clojure's core library, to spot little gems like this one, and to have added a new tool to your toolbox!

Code for this post can be found here.