This blog starts with a brief definition of Test Driven Development. Then we recall the basic goals of programming and understand how TDD is essential for achieving these goals. Then we’ll see a very basic hands-on example of TDD in Python.

Definition

Test driven development is a style of development where you write the test cases beforehand and then run them. What happens next? You see all your test cases failing as you've not implemented anything. Now you start writing code to make the test cases pass and as all of the test cases pass, you're done. You've come up with a correct version of code at the first place. Now you may refactor the code to optimize it further and make sure that it still works correctly by running the test cases again. Fore more information on TDD, click here.

3 Goals of Programming

Let’s get back to the basics and recall our basic goals while we’re writing code:

  1. The code should be correct, today and in the unknown future
  2. The code should be readable for other programmers as well as future You
  3. The code should be extensible i.e. ready to incorporate new changes without a re-write

Goal #1

We achieve this with TDD as we write the correct version of the code in the first place instead of debugging it later.

Goal #2

Usually we break the test cases into different functions with each of them explaining the aspect of specification they are testing. This makes reading the code easier as you know what the code is expected to do rather than reading the standalone code and guessing what it is supposed to do.

Goal #3

When you have a test suite with a decent coverage. You can extend the functionality and test it regressively to ensure that the existing functionality is not breaking. Then add your test cases to test the new functionality as well.

TDD Example

Most of us start to learn coding with a simple print("Hello, World!") program. But how do we get to know if we have written it correctly or not? Well, it's simple, we run the program and see the result.

Then, on a later stage, if we are asked to write a program to find if N is a prime number or not. We build our logic and implement it on code. To check if it's correct or not, we try it on some random inputs. If the output is correct, we assume the program to be correct. And that's where we go wrong. We develop a habit of writing code and then test it manually for some random inputs or some edge cases. As we get experienced with programming, we become so confident that we omit the testing part at all. Below is one such program:

def is_prime(n):
    """
    :param n: int, greater than or equal to 1
    :return: bool, True if n is prime else False
    """
    for i in range(2, n):
        if n % i == 0:
            return False
    return True

print(is_prime(3))
#=> prints 'True'
print(is_prime(4))
#=> prints 'False'
print(is_prime(5))
#=> prints 'True'
print(is_prime(557))
#=> prints 'True'

I can assume it to be correct on the basis of my testing. But when I give 1 as input, it fails as shown below:

print(is_prime(1))
#=> prints 'True'

Had we written test cases at the first place, we could have avoided this bug. Below is the TDD approach for the same program. For simplicity, I've not used Python's unittest module and instead wrote a simple function test_is_prime to test the is_prime function.
Notice how we have kept the definition of is_prime empty on purpose.

def is_prime(n):
    """
    :param n: int, greater than or equal to 1
    :return: bool, True if n is prime else False
    """
    pass


def test_is_prime():
    """
    Tests the function is_prime for some set of inputs -> {1, 2, 3, 4, 5, 100, 101, 557}
    """
    prime_nums = {2, 3, 5, 101, 557}
    non_prime_nums = {1, 4, 100, 553}

    for n in prime_nums:
        result = is_prime(n)
        assert result is True, "{} is a prime number while `is_prime` returned `False`".format(n)

    for n in non_prime_nums:
        result = is_prime(n)
        assert result is False, "{} is not a prime number while `is_prime` returned `True`".format(n)
    print("Tested `is_prime` on input set -> {}".format(prime_nums.union(non_prime_nums)))
    print("Ok")

test_is_prime()

#=> Output:
Traceback (most recent call last):
  File "is_prime.py", line 34, in <module>
    test_is_prime()
  File "is_prime.py", line 24, in test_is_prime
    assert result is True, "{} is a prime number while `is_prime` returned `False`".format(n)
AssertionError: 2 is a prime number while `is_prime` returned `False`

Let's change is_prime definition and run the tests again:

def is_prime(n):
    """
    :param n: int, greater than or equal to 1
    :return: bool, True if n is prime else False
    """
    for i in range(2, n):
        if n % i == 0:
            return False
    return True

test_is_prime()
#=> Output:
Traceback (most recent call last):
  File "is_prime.py", line 33, in <module>
    test_is_prime()
  File "is_prime.py", line 27, in test_is_prime
    assert result is False, "{} is not a prime number while `is_prime` returned `True`".format(n)
AssertionError: 1 is not a prime number while `is_prime` returned `True`

Now fixing this 1 as input problem:

def is_prime(n):
    """
    :param n: int, greater than or equal to 1
    :return: bool, True if n is prime else False
    """
    if n == 1:
        return False
    for i in range(2, n):
        if n % i == 0:
            return False
    return True

test_is_prime()
#=> Output:
Tested `is_prime` on input set -> {1, 2, 3, 100, 101, 5, 4, 553, 557}
Ok

Now we can be confident that our program is working for the mentioned input set. If we ever need to test the funtion for some more inputs, we just need to add them to test_is_prime definition. This makes testing the program an automated process which can be repeated n number of times without any manual intervention. Also, by writing program in such a manner, we avoid the pain of debugging the program later as we've written the program correct in the first place. That's why we say:

Testing rocks, debugging sucks.

The source code of the final version of the program that we wrote above is available below. Download and try it yourself:

source code

You can email me any questions/suggestions regarding this blog on my email address or ping me on the networking links provided below in the footer.