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:
class TestArea(unittest.TestCase): def test_postive_numbers(self): self.assertAlmostEqual(circleArea(4), 3.142*4*4) self.assertEqual(circleArea(0), 0)
py -m unittest
python3 -m unittest
. ---------------------------------------------------------------------- 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)
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)
.. ---------------------------------------------------------------------- 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")
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)
... ---------------------------------------------------------------------- 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)