Test-driven development implies that we first write unit tests for our modules before writing the module itself. This way we know what we expect from our function and then write the function so that it passes the test cases. There are a total of 3 steps involved in TDD (Test-Driven Development).
- Red - You write test cases first which fails
- Green - You then write just enough code that passes the test cases.
- Refactor - Now you improve the code quality
For a demonstration of TDD, we will be writing a simple function that calculates the area of a rectangle.
def rect(length,breadth):
return length*breadth
Although this function looks good the reality is that it is full of bugs let's try it out.
def rect(length,breadth):
return length*breadth
print(rect(5,10)) #Output: 50
print(rect(4.5,3.4)) #Output: 15.29
print(rect(3,"HELLO")) #Output: HELLOHELLOHELLO
print(rect(-3,2)) #Output: -6
print(rect(0,5)) #Output: 0
As we can see when we pass a string with an integer, a negative value or zero it handles them incorrectly and we get unexpected results. No side of a rectangle can have a negative value or zero nor should it accept a string value. This is where unit testing comes into place. Before writing code we first write some of the test cases.
Create a new file and make sure you add test_
prior to the name for example, if we assume our python file's name is app.py
then our test file should be test_app.py
.
Write the following test cases
from unittest import TestCase #import TestCase class from unittest module
from app import rect #import your rect function from app.py you wrote
class testApp(TestCase): #make a class which inherits the TestCase class
'''Test rect function'''
from unittest import TestCase #import TestCase class from unittest module
from app import rect #import your rect function from app.py you wrote
class testApp(TestCase): #make a class which inherits the TestCase class
'''Test rect function'''
def test_area_of_rectangle(self):
'''Test the area of a rectangle'''
self.assertEqual(rect(5,10),50)
def test_bad_value(self):
'''Test if the function rejects invalid values'''
self.assertRaises(ValueError, rect,-5,3)
self.assertRaises(ValueError, rect,0,2)
def test_bad_types(self):
'''Test if the function rejects invalid types'''
self.assertRaises(TypeError, rect,"Hello", 3)
self.assertRaises(TypeError, rect,True, 5)
The first test case will check if the function is returning the expected result when passing good values.
This is an example of 'Happy paths' in testing which tests if the function returns the expected result.
The next two test cases check if the function handles exceptions when passed bad data.
This is an example of 'Sad paths' in testing which tests if the function's exception handlers works properly when bad data is passed.
You can either use the default test-runner that comes with Python i.e unittest.
go to the file location and type in python -m unittest discover
this will run all the test cases. If the test case pass then it will show '.' else it will show 'E' along with the test case name with which failed. In our case if we run this we get:
So, two of the tests failed but the output was not so helpful. let's try another test runner nosetests
But we need few other things as well, pinocchio
for organized output and coloured text and coverage
for code coverage report
We need coverage to ensure that every code statement e.g if-else branches etc. are tested before pushing it to production. The code coverage should be as high as possible.
Install:
pip install nose pinocchio coverage
Troubleshooting: If you are using python 3.10 or above, then
nosetests
won't work directly and will give you error. So you need to make changes to the following files:
.local/lib/python3.10/site-packages/nose/suite.py
.local/lib/python3.10/site-packages/nose/case.py
you have to change two things, first
changeimport collections
toimport collections.abc
and then changecollections.Callable
tocollections.abc.Callable
wherever you see the statement.
To run nosetests
along with pinocchio
and coverage
nosetests --with-spec --spec-color --with-coverage
The test cases highlighted in red are the ones that failed and the one highlighted in green has passed. We can also see that the code coverage for the file is 100% which is perfect. We can see that the exceptions were not raised. So let's update our code.
def rect(length,breadth):
if type(length) not in [int,float] or type(breadth) not in [int,float]:
raise TypeError("Length or Breadth must be int or float")
return length*breadth
We have added a statement to raise TypeError if the type of length or breadth is not integer or float. Let's try out our test cases.
Great! test_bad_types has passed but still, the test_bad_values is not raising an exception when it is getting a negative or zero value for length or breadth. Let's modify our code again.
def rect(length,breadth):
if type(length) not in [int,float] or type(breadth) not in [int,float]:
raise TypeError("Length or Breadth must be int or float")
if length<=0 or breadth<=0:
raise ValueError("Lenght or Breadth cannot be zero or negative")
return length*breadth
Now we have added a statement to raise ValueError when we receive negative or zero values for length or breadth. Let's try our test cases once more.
Congratulations! All the test cases have been passed, the happy paths and the sad paths have been tested, the code coverage is 100% and our rect
function is now ready to be pushed in production.
Quick tip: The nosetests
command is a long one and everytime you run it, you need to pass all the required flags to trigger pinocchio and coverage. So add a file setup.cfg
which will contain the following:
[nosetests]
verbosity=2
with-spec=1
spec-color=1
with-coverage=1
cover-package=app
[coverage:report]
show_missing = True
I have also added show_missing = True
parameter which will pinpoint the line numbers which are not tested. Now just run nosetests
without any parameter or flags, it will fetch all the required configuration flags and parameters from setup.cfg
and will provide the same result.
So we now have understood the idea of TDD. Write test cases first and let your unit test derive your code.
Thank you,
Happy Coding!
Top comments (0)