Data driven unit tests with Clojure
Francesco
Posted on May 2, 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 string Fizz if the number is divisible by 3
- The string Buzz if the number is divisible by 5
- The string FizzBuzz if the number is divisible by both 3 and 5
- The argument if none of the previous conditions are met
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!
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.
Posted on May 2, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.