Test Driven Development

Unit Testing Basics in Python in Test Driven Development Environment

Introduction

At one point, I came across a company, for which I was applying for a position. I was asked to build a Tic Tac Toe game, but here was the challenge: I have to maintain SOLID design principles and the code has to be tested properly. Here the S stands for Single Responsibility Principle, which means that a code class should have only one task to do and that was it. If I remember, I had to create more than 12 classes for the tic-tac-toe game. Focusing on having singular responsibility has its advantages and one of them is that you can test each class properly. This is where my topic on Unit test comes. Unit testing is very important, especially when you are doing test driven development, where developers write functions and test code to test that function simultaneously. In this topic, I will explain how to do unit testing on Python code using a library called unittest. So without any further delay, and assuming you have Python 3 already installed in the computer (either Windows or Mac), lets start.

Procedure

Lets have a folder called unittest. Inside that called we have a file called calculator.py with the following two lines of code:

def circleArea(input):

    return 3.142*(input**2)

The file contains a method which will calculate the area of the circle of a radius that is passed on the single parameter of the circleArea function. As you know that running this single file will not execute anything since we did not execute the circleArea() function. Feel free to add something like circleArea(4) at the end of the file and run on the terminal using the following:

On Windows:

py calculator.py

On Mac:

python3 calculator.py

While experimenting with different inputs, you will notice that there will be ValueError and TypeError, if you add any input other than just positive number. This is where we will do some unit testing to make sure the circleArea() function works properly using any input that you provide. Based on the unittest results, we will modify calculator.py accordingly. This is a part of test driven development or TDD.

On the same folder, create a new file called test_calculator.py (make sure you always include test_ prefix on the file so that python can recognize it as a unittest file). Add the following codes:

import unittest
from calculator import circleArea
class TestArea(unittest.TestCase):

    def test_postive_numbers(self):

        self.assertAlmostEqual(circleArea(4), 3.142*4*4)

        self.assertEqual(circleArea(0), 0)
What we did here is that we have imported unittest library to use its test functions amd imported the squareRoot method from the calculator.py. We have created a class to test Square Root and added three tests. assertEqual matches exact output of the expected result and the actual result. assertAlmostEqual matches the floating number of expected value and actual value upto 7 decimal places (this is useful if floating numbers are needed to be tested). If we run the test in the terminal by applying the following command:
On Windows:
py -m unittest
On Mac:
python3 -m unittest
We will see the following result:
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

The single dot represents the test. The result says OK and so it is good for inputting positive numbers. However the circleArea function will work when we put negative number, although theoretically radius of a circle cannot be negative. Now lets go deeper and add addition test for inputting negative number:

import unittest

from calculator import circleArea

class TestArea(unittest.TestCase):

    def test_postive_numbers(self):

        self.assertAlmostEqual(circleArea(4), 3.142*4*4)

        self.assertEqual(circleArea(0), 0)




    def test_negative_numbers(self):

        self.assertRaises(ValueError, circleArea, -2)
If we run the test the the above code will result in the following:
F.
======================================================================
FAIL: test_negative_numbers (test_calculator.TestArea)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\Users\junction\Downloads\Unittest\test_calculator.py", line 11, in test_negative_numbers
self.assertRaises(ValueError, circleArea, -2)
AssertionError: ValueError not raised by circleArea

----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)

This implies that one test has failed, which denotes F and it also says which test Failed. As we have added assertRaises() function which takes the exception, the function and function parameter as arguments, the code tests out whether exception has been raised or not. Here the exception has not been raised and so it failed. Lets modify the calculator.py code into following:

def circleArea(input):

    if input < 0:

        raise ValueError("The radius cannot be negative")

    return 3.142*(input**2)
If we run test_calculator.py now, the following will show:
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

All tests pass with two dots.

Lets go even deeper.  Lets modify test_calculator.py to add more tests to test whether user accidentally inputted string or boolean values:

import unittest

from calculator import circleArea

class TestArea(unittest.TestCase):

    def test_postive_numbers(self):

        self.assertAlmostEqual(circleArea(4), 3.142*4*4)

        self.assertEqual(circleArea(0), 0)




    def test_negative_numbers(self):

        self.assertRaises(ValueError, circleArea, -2)

    def test_input_types(self):

        self.assertRaises(TypeError, circleArea, True)

        self.assertRaises(TypeError, circleArea, "Syed")
If we run this now, the test result will show:
F..
======================================================================
FAIL: test_input_types (test_calculator.TestArea)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\Users\junction\Downloads\Unittest\test_calculator.py", line 14, in test_input_types
self.assertRaises(TypeError, circleArea, True)
AssertionError: TypeError not raised by circleArea

----------------------------------------------------------------------
Ran 3 tests in 0.001s

FAILED (failures=1)

This implies that the input types test failed because there was no TypeError exception raised. Lets modify the calculator.py function:

def circleArea(input):

    if type(input) not in [int, float]:

        raise TypeError("The radius should be a number")

    if input < 0:

        raise ValueError("The radius cannot be negative")

    return 3.142*(input**2)
Finally if we run the test_calculator.py file, we will see the following result:
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

This indicates all three tests passed and we finally have a fully tested calculator.py file. Give yourself a pat on the back.

Conclusion

The TDD practice has been gradually adopted to many software industries, as a means of developing codes with less bugs. Although we do need black box testing on the quality assurance side to make sure there is enough test coverage but to save some time in the development finding bugs right during the development stage, TDD practice is very useful. Python has many unit test libraries and this is one of them. The stuffs that I covered here are basics but if you need to know how many assert functions are there, you can always refer here or you can type in the terminal:

On Windows (Getting reference for assertSetEqual):

py
import unittest
help(unittest.TestCase.assertSetEqual)

On Mac (Getting reference for assertSetEqual):

python3
import unittest
help(unittest.TestCase.assertSetEqual)