DEV Community

Francesco
Francesco

Posted on

Data driven unit tests with Clojure

original article + updates

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))))
      )
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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"))
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (2)

Collapse
 
fpsd profile image
Francesco

Please, consider sharing the original post from my website as well! I am not running ADS or trying to monetize it, I just want to monitor traffic coming to it.

fpsd.codes/clojure-bites---clojure...

Collapse
 
fpsd profile image
Francesco

updated the post with the feedback received on Reddit reddit.com/r/Clojure/comments/135d...