Test-Driven Development in Python using Unittest and Nose
Tags: testing, python
Published: 2024-06-28 15:36:47

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

1. Red - You write test cases first which fails
2. Green - You then write just enough code that passes the test cases.
3. 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:


![Running test cases with unittest](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qamupk9ovmiamdtet4k0.png)


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`

To run `nosetests` along with `pinocchio` and `coverage`
`nosetests --with-spec --spec-color --with-coverage`


![Running test cases with nosetests](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7ip6mn757b7bmgfisdr9.png)

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.

![Passing test case 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9qqq08semh0i3wwrl2gq.png)

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.


![Passing test case 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p2oh2w4xb1zefx9nzrla.png)

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.


![Nostest with setup.cfg](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kt1llqx4lbcwai6cjxlp.png)



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!