Test Driven Development (TDD) for dummies – We perform this programming technique to develop functionality of our crafter.ai chatbot builder platform.

In this article we want to explain why you should use this programming technique to develop your products or services.

AUTOMATIC TEST DEFINITION

An automatic test is a script which verifies the aptitude of a code chunk.

In this article we won’t focus on the kinds of tests but we’ll create unique tests to validate our function. We’ll use the unittest python bookshop already included in the language standard.

DEFINITION OF THE TEST DRIVEN DEVELOPMENT

Test Driven Development (TDD) is a software development technique in which tests are first written and then the code that satisfies them.

ADVANTAGES OF TEST DRIVEN DEVELOPMENT

CODE COVERED BY TEST

This procedure allows you to always have a code that is well covered by testing and therefore more maintainable.

FOCUS ON THE goal

The written tests have the task of covering all the requests, so when we go to write the code we have already defined its behavior precisely.

DOCUMENTATION

By developing the tests on the required functionality we will have that the tests themselves will document the code. In addition, the tests are updated during code modification, eliminating the problem of updating the documentation.

EXAMPLE OF TEST DRIVEN DEVELOPMENT

In this first article I want to immediately bring with an example of Test Driven Development starting from the FizzBuzz Kata

ACCEPTANCE CRITERIA

For the complete test of the kata, I refer you to the link indicated above. For convenience, I report the acceptance criteria that our method must have to overcome the first part of the Kata:

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

1. NUMBERS NOT DIVISIBLE BY 3 AND / OR 5

First we define the say_number function with no behavior and the first test required.

import unittest

def say_number(number):
    pass

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


if __name__ == '__main__':
    unittest.main()

Executing the code the test breaks so let’s write the code that satisfies our test

def say_number(number):
    return str(number)

By running the tests we can see that our code passes them.

NOTE

The number of assertions we are going to make is limited, it must be a reasonable number to cover a good number of cases but they can never all be covered. It is possible to provide tests with random numbers but this would greatly complicate the writing of the tests, take care of any problems only after they arise!

2. MULTIPLES OF THREE

So let’s add a new method to our test class

def test_return_fizz(self):
    self.assertEqual(say_number(3), 'fizz')

we proceed in stages adding only the control on number three and we go to satisfy our test

def say_number(number):
    if number == 3:
        return 'fizz'
    return str(number)

Of course the code satisfies the test but does not satisfy the request. In the real world, such behavior would generate a bug. For example, let’s create a simple script that uses our code:

for number in range(100):
    print(say_number(number))

running our script we get:

1
2
fizz
4
5
6 # ERROR

So we found a bug, what to do? Let’s add the bug case to our test!

def test_return_fizz(self):
    self.assertEqual(say_number(3), 'fizz')
    self.assertEqual(say_number(6), 'fizz')

We have rebuilt the bug through a self-test, passing the test will fix the bug

def say_number(number):
    if number == 3 or number == 6:
        return 'fizz'
    return str(number)

We run the tests and this time too they are passed. For safety, however, this time we add new assertions:

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')

We realize that there is something wrong, we must look for a better strategy and avoid continuing to manage every single multiple of 3

def say_number(number):
    if (number % 3) == 0:
        return 'fizz'
    return str(number)

Perfect, now even running our script function we can see that all multiples of 3 are managed.

3. MULTIPLES OF 5

Let’s move to multiples of 5 then and write the tests as always before:

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')

and then we update our function:

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

4. MULTIPLES OF 3 AND 5

let’s move on to the last point of our requests as always, let’s write our tests:

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

and our code:

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

but now running the tests we immediately see that our code does not pass them, in fact say_number (15) returns “fizz” and not “fizzbuzz”. Analyzing the code we immediately realize that being divisible by 3 we enter the first if and never get to the third. To solve this problem, simply move the last if first

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

passing the tests. The more attentive will note that the first if can be rewritten since if a number is divisible by 3 and 5 it will surely be a multiple of 15. Let’s do this little refactoring:

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)

The tests will make sure that the change made is correct and that the behavior of our method continues to be the same for the values we are testing it for.

CONCLUSIONS ON THE TEST DRIVEN DEVELOPMENT

Of course the code could be refactored and written better but this was just an example to familiarize yourself with the development method.

We will then continue the kata by implementing the rest of the requests and subsequently rewriting the code.

And as a senior essay told me when I was starting to use the Test Driven Development method:

“It doesn’t matter when you test, the important thing is that you test” – cited Matteo Galacci