FizzBuzz Kata Guide Part 2 – We have arrived at our second part of our article on Test Driven Development.

In the first article we started working on the FizzBuzz kata and we saw how to develop with the tdd technique.

Today we will focus on a first part of refactoring where we will see how useful the tests can be and then we will concentrate on developing the second part of the kata.

FIZZBUZZ KATA GUIDE: REFACTORING

This is our function at the end of the first article:

def say_number(number):
    if (number % 15) == 0:
        return 'fizzbuzz'
    if (number % 3) == 0:
        return 'fizz'
    if (number % 5) == 0:
            return 'buzz'
    return str(number)

EXPLANATORY FUNCTION

Undoubtedly, the expressions within our if make the code unreadable, in a first step we can think of creating a support function “is_divisible_by” which takes two numbers as parameters

def is_divisible_by(dividend, divider):
    return dividend % divider == 0

The function in this case is trivial, as in the expressions inside the if we are going to check that the rest of the division is zero in order to understand if a number is a multiple of another.

We use our new function in our “say_number”

def say_number(number):
    if is_divisible_by(number, 15):
        return 'fizzbuzz'
    if is_divisible_by(number, 3):
        return 'fizz'
    if is_divisible_by(number, 5):
        return 'buzz'
    return str(number)

By running the tests we can see that the behavior of our function remains unchanged.

REMOVAL OF IF

An if is a point where the program can have 2 different behaviors, this forces the reader to understand what each path does. It is always preferable, for a question of clarity, to reduce the ifs to the bare minimum.

We can remove the if’s by creating a mapping function that maps a divider to its specific sound.

from functools import reduce

def get_sound(number):
    mappers = [
        {'number': 3, 'sound': 'fizz'},
        {'number': 5, 'sound': 'buzz'},
    ]
    return reduce(
        lambda acc, mapper: acc + mapper['sound'],
        filter(
            lambda mapper: is_divisible_by(number, mapper['number']),
            mappers
        ),
        ""
    )

The code here becomes a bit more complex but nothing arcane, the “filter” function returns only the mappers that have the “number” parameter that divides the given number, the reduce combines all the strings of the sounds.

Having developed the tests first, they will allow us to quickly test if our code is correct.

For example, during the development of this kata I discovered that the reduce in python has a third parameter to indicate the initial value of the accumulator. In fact, the tests were not passed and this also allowed me to discover new things about the language I was using without generating bugs in production.

We also update the code of the “say_number” function

def say_number(number):
    return get_sound(number) or str(number)

Passed tests, we can go to the second point of the kata.

FIZZBUZZ KATA GUIDE – NEW REQUESTS

We then come to the second point of the kata and let’s update our new acceptance criteria

ACCEPTANCE CRITERIA

  1. if the number passed in input is not a multiple of 3 and / or 5 or does not contain them, then the function must return that number
  2. if the number passed in input is a multiple of 3i or contains it then the function will return the string ‘fizz’
  3. if the number in input is multiplied by 5i or contains it, the function will return the string ‘buzz’
  4. if the input number is a multiple of 3 and 5 or contains them, the function will return ‘fizzbuzz’

CHANGES

With the refactoring that we have performed, completing these new requests is much easier but let’s go in order.

Let’s write our new tests:

class TestSayNumber(unittest.TestCase):
    def test_return_number_string(self):
        self.assertEqual(say_number(1), '1')

    def test_return_fizz(self):
        self.assertEqual(say_number(3), 'fizz')
        self.assertEqual(say_number(6), 'fizz')
        self.assertEqual(say_number(9), 'fizz')
        self.assertEqual(say_number(12), 'fizz')
        self.assertEqual(say_number(13), 'fizz')
        self.assertEqual(say_number(31), 'fizz')
        self.assertEqual(say_number(1234), 'fizz')

    def test_return_buzz(self):
        self.assertEqual(say_number(5), 'buzz')
        self.assertEqual(say_number(10), 'buzz')
        self.assertEqual(say_number(20), 'buzz')
        # self.assertEqual(say_number(35), 'buzz') removed
        self.assertEqual(say_number(52), 'buzz')
        self.assertEqual(say_number(152), 'buzz')
        self.assertEqual(say_number(5254), 'buzz')

    def test_return_fizzbuzz(self):
        self.assertEqual(say_number(15), 'fizzbuzz')
        self.assertEqual(say_number(30), 'fizzbuzz')
        self.assertEqual(say_number(60), 'fizzbuzz')
        self.assertEqual(say_number(53), 'fizzbuzz')
        self.assertEqual(say_number(1354), 'fizzbuzz')

Note the removed test, the 35 with the new requests must return fizzbuzz.

To pass our new tests, just create a new function to check the inclusion of a number within another:

def contain_number(container, part):
    return str(part) in str(container)

And add it to our mapping function:

def get_sound(number):
    mappers = [
        {'number': 3, 'sound': 'fizz'},
        {'number': 5, 'sound': 'buzz'},
    ]
    return reduce(
        lambda acc, mapper: acc + mapper['sound'],
        filter(
            lambda mapper: is_divisible_by(number, mapper['number']) or contain_number(number, mapper['number']),
            mappers
        ),
        ""
    )

We pass the tests and finish the kata.

CONCLUSIONS ON THE SECOND PART OF FIZZBUZZ KATA’S GUIDE

Tests can show us the way and give us a safety net as we develop.

The tdd teaches us that before thinking about writing the code it is good to clarify what we want from our functions and prevents the delay of writing the tests.