DEV Community

Cover image for Playing Fizzbuzz with property-based testing
Manuel Rivero
Manuel Rivero

Posted on • Updated on • Originally published at codesai.com

Playing Fizzbuzz with property-based testing

Introduction.

Lately, I've been playing a bit with property-based testing.

I practised doing the FizzBuzz kata in Clojure and used the following constraints for fun[1]:

  1. Add one property at a time before writing the code to make the property hold.
  2. Make the failing test pass before writing a new property.

The kata step by step.

To create the properties, I partitioned the first 100 integers according to how they are transformed by the code. This was very easy using two of the operations on sets that Clojure provides (difference and intersection).

The first property I wrote checks that the multiples of 3 but not 5 are Fizz:

(ns fizzbuzz-pbt.core-test
  (:require
    [midje.sweet :refer :all]
    [clojure.set :as set]
    [clojure.test.check.generators :as gen]
    [midje.experimental :refer [for-all]]
    [fizzbuzz-pbt.core :as sut]))

(defn- multiples-of [n]
  (iterate #(+ % n) n))

(defn- fizzbuzz-for [n]
  (nth (sut/fizzbuzz) (dec n)))

(def multiples-of-3 
  (set (take-while #(< % 101) (multiples-of 3))))

(def multiples-of-5 
  (set (take-while #(< % 101) (multiples-of 5))))

(facts
  "about fizzbuzz"

  (fact
    "multiples of 3 but not 5 are Fizz"
    (for-all
      [n (gen/elements
           (set/difference
             multiples-of-3
             multiples-of-5))]
      {:num-tests 100}
      (fizzbuzz-for n) => "Fizz")))
Enter fullscreen mode Exit fullscreen mode

and this is the code that makes that test pass:

(ns fizzbuzz-pbt.core)

(defn fizzbuzz []
  (take 100 (cycle ["" "" "Fizz"])))
Enter fullscreen mode Exit fullscreen mode

Next, I wrote a property to check that the multiples of 5 but not 3 are Buzz (I show only the new property for brevity):

(ns fizzbuzz-pbt.core-test
  (:require
    ;; other requires
    [fizzbuzz-pbt.core :as sut]))

;; ...
;; some helpers
;; ...

(facts
  "about fizzbuzz"
 ;; ...
 ;; previous properties
 ;; ...

  (fact
    "multiples of 5 but not 3 are Buzz"
    (for-all
      [n (gen/elements
           (set/difference
             multiples-of-5
             multiples-of-3))]
      {:num-tests 100}
      (fizzbuzz-for n) => "Buzz")))
Enter fullscreen mode Exit fullscreen mode

and this is the code that makes the new test pass:

(ns fizzbuzz-pbt.core)

(defn fizzbuzz []
  (take 100 (map #(str %1 %2)
                 (cycle ["" "" "Fizz"])
                 (cycle ["" "" "" "" "Buzz"]))))
Enter fullscreen mode Exit fullscreen mode

Then, I added a property to check that the multiples of 3 and 5 are FizzBuzz:

(ns fizzbuzz-pbt.core-test
  (:require
    ;; other requires
    [fizzbuzz-pbt.core :as sut]))

;; ...
;; some helpers
;; ...

(facts
  "about fizzbuzz"

  ;; ...
  ;; previous properties
  ;; ...

  (fact
    "multiples of 3 and 5 are FizzBuzz"
    (for-all
      [n (gen/elements
           (set/intersection
             multiples-of-5
             multiples-of-3))]
      {:num-tests 100}
      (fizzbuzz-for n) => "FizzBuzz")))
Enter fullscreen mode Exit fullscreen mode

which was already passing with the existing production code.

Finally, I added a property to check that the rest of numbers are just casted to a string:

(ns fizzbuzz-pbt.core-test
  (:require
    ;; other requires
    [fizzbuzz-pbt.core :as sut]))

;; ...
;; some helpers
;; ...

(facts
  "about fizzbuzz"

  ;; ...
  ;; previous properties
  ;; ...

  (fact
    "the rest of numbers are casted to string"
    (for-all
      [n (gen/elements
           (set/difference
             (set (range 1 101))
             multiples-of-3
             multiples-of-5))]
      {:num-tests 100}
      (fizzbuzz-for n) => (str n))))
Enter fullscreen mode Exit fullscreen mode

which Id made pass with this version of the code:

(ns fizzbuzz-pbt.core
  (:require
    [clojure.string :as string]))

(defn fizzbuzz []
  (map #(string/replace (str %1 %2) #"^$" (str %3))
       (cycle ["" "" "Fizz"])
       (cycle ["" "" "" "" "Buzz"])
       (range 1 101)))
Enter fullscreen mode Exit fullscreen mode

The final result.

These are the resulting tests where you can see all the properties together:

(ns fizzbuzz-pbt.core-test
  (:require
    [midje.sweet :refer :all]
    [clojure.set :as set]
    [clojure.test.check.generators :as gen]
    [midje.experimental :refer [for-all]]
    [fizzbuzz-pbt.core :as sut]))

(defn- multiples-of [n]
  (iterate #(+ % n) n))

(defn- fizzbuzz-for [n]
  (nth (sut/fizzbuzz) (dec n)))

(def multiples-of-3 
  (set (take-while #(< % 101) (multiples-of 3))))

(def multiples-of-5 
  (set (take-while #(< % 101) (multiples-of 5))))

(facts
  "about fizzbuzz"

  (fact
    "multiples of 3 but not 5 are Fizz"
    (for-all
      [n (gen/elements
           (set/difference
             multiples-of-3
             multiples-of-5))]
      {:num-tests 100}
      (fizzbuzz-for n) => "Fizz"))

  (fact
    "multiples of 5 but not 3 are Buzz"
    (for-all
      [n (gen/elements
           (set/difference
             multiples-of-5
             multiples-of-3))]
      {:num-tests 100}
      (fizzbuzz-for n) => "Buzz"))

  (fact
    "multiples of 3 and 5 are FizzBuzz"
    (for-all
      [n (gen/elements
           (set/intersection
             multiples-of-5
             multiples-of-3))]
      {:num-tests 100}
      (fizzbuzz-for n) => "FizzBuzz"))

  (fact
    "the rest of numbers are casted to string"
    (for-all
      [n (gen/elements
           (set/difference
             (set (range 1 101))
             multiples-of-3
             multiples-of-5))]
      {:num-tests 100}
      (fizzbuzz-for n) => (str n))))
Enter fullscreen mode Exit fullscreen mode

You can find all the code in this repository.

Conclusions.

It was a lot of fun doing this kata. It is a toy example that didn't make me dive a lot into clojure.check's generators documentation because I could take advantage of Clojure's set functions to write the properties.

I think the resulting properties are quite readable even if you don't know Clojure. On the other hand, the resulting implementation is probably not similar to the ones you're used to see, and it shows Clojure's conciseness and expressiveness.

Footnotes:

[1] I'm not saying that you should do property-based testing with this constraints. They probably make no sense in real cases. The constraints were meant to make solving the kata fun.

Top comments (1)

Collapse
 
mebble profile image
Neil Syiemlieh

This was great. I’ve been learning clojure for 3 months and I’m loving it. I’ll be coming back here when I experiment with property based testing.