Data driven unit tests with Clojure
Tags: clojure, unittest, beginners, programming
Published: 2023-05-02 06:10:54

[original article + updates](https://fpsd.codes/clojure-bites---clojuretestare.html)

# Overview

Clojure's [clojure.test/are](https://clojuredocs.org/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](https://en.wikipedia.org/wiki/Fizz_buzz) 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](https://docs.pytest.org/en/7.3.x/) may be familiar with the [parametrize](https://docs.pytest.org/en/7.3.x/how-to/parametrize.html#pytest-mark-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](https://gitlab.com/fpischedda/fp-site/-/merge_requests/8/diffs).