Tutorials
python

Unit Testing in Python

Learn what unit testing is, why its important, and how you can implement it with the help of Python.

If you are just getting started in Python and would like to learn more, take DataCamp's Introduction to Data Science in Python course.

Source

Unit testing is a software testing method by which individual units of source code are put under various tests to determine whether they are fit for use (Source). It determines and ascertains the quality of your code.

Generally, when the development process is complete, the developer codes criteria, or the results that are known to be potentially practical and useful, into the test script to verify a particular unit's correctness. During test case execution, various frameworks log tests that fail any criterion and report them in a summary.

The developers are expected to write automated test scripts, which ensures that each and every section or a unit meets its design and behaves as expected.

Though writing manual tests for your code is definitely a tedious and time-consuming task, Python's built-in unit testing framework has made life a lot easier.

The unit test framework in Python is called unittest, which comes packaged with Python.

Unit testing makes your code future proof since you anticipate the cases where your code could potentially fail or produce a bug. Though you cannot predict all of the cases, you still address most of them.

A unit could be bucketed into various categories:

  • An entire module,
  • An individual function,
  • A complete interface like a class or a method.

The best ways to write unit tests for your code is to first start with a smallest testable unit your code could possibly have, then move on to other units and see how that smallest unit interacts with other units, this way you could build up a comprehensive unit test for your applications.

Python's unit testing framework was inspired by java's JUnit and has similar features as major unit testing frameworks in other languages. Python's unit testing framework offers various features like (Source: TutorialsPoint)

  • Test automation
  • Sharing of setup and shutdown code for tests
  • Aggregating tests into collections
  • Independence of the tests from the reporting framework

Now let's take an example and understand why there would be a need to have a unit testing of your code.

Let's write a code to calculate the volume of a cube in Python.

def cuboid_volume(l):
    return (l*l*l)
length = [2,1.1, -2.5, 2j, 'two']
for i in range(len(length)):
    print ("The volume of cuboid:",cuboid_volume(length[i]))
The volume of cuboid: 8
The volume of cuboid: 1.3310000000000004
The volume of cuboid: -15.625
The volume of cuboid: (-0-8j)



---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-7-67e1c37a435f> in <module>
      1 for i in range(len(length)):
----> 2     print ("The volume of cuboid:",cuboid_volume(length[i]))


<ipython-input-2-f50464fd88da> in cuboid_volume(l)
      1 def cuboid_volume(l):
----> 2     return (l*l*l)


TypeError: can't multiply sequence by non-int of type 'str'

The above output should give you some intuition about the importance of having a unit test in place for your code. Now there are three things which are certainly incorrect in the above code:

  • First, the volume of cuboid being negative,
  • Second, the volume of the cuboid is a complex number,
  • Finally, the code resulting in a TypeError since you cannot multiply a string, which is a non-int.

The third problem thankfully resulted in an error while the first & second still succeeded even though the volume of the cuboid cannot be negative and a complex number.

Unit tests are usually written as a separate code in a different file, and there could be different naming conventions that you could follow. You could either write the name of the unit test file as the name of the code/unit + test separated by an underscore or test + name of the code/unit separated by an underscore.

For example, let's say the above code file name is cuboid_volume.py, then your unit test code name could be cuboid_volume_test.py

Without any further ado, let's write the unit test for the above code.

First, let's create a python file with the name volume_cuboid.py, which will have the code for calculating the volume and second with the name test_volume_cuboid.py, which will have the unit testing code.

import unittest

The TestCuboid class inherits the unittest module, and in this class, you would define various methods that you would want your unit test should check with your function cuboid_volume.

The first function you will define is test_volume, which will check whether the output your cuboid_volume gives is equal to what you expect. To achieve this, you will make use of the assertAlmostEqual method.

class TestCuboid(unittest.TestCase):
    def test_volume(self):
        self.assertAlmostEqual(cuboid_volume(2),8)
        self.assertAlmostEqual(cuboid_volume(1),1)
        self.assertAlmostEqual(cuboid_volume(0),0)
        self.assertAlmostEqual(cuboid_volume(5.5),166.375)

Let's run the above script. You would run the unittest module as a script by specifying -m while running it.

!python -m unittest test_volume_cuboid.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

Great! So you got your first unit test code working.

The test ran successfully and returned, OK, which means the cuboid_volume function works as you would expect it too.

Let's see what happens when one of the assertAlmostEqual methods fails.

Notice that the last assert statement has been modified.

!python -m unittest test_volume_cuboid.py
F
======================================================================
FAIL: test_volume (test_volume_cuboid.TestCuboid)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\Users\hda3kor\Documents\Unit_Testing_Python\test_volume_cuboid.py", line 15, in test_volume
    self.assertAlmostEqual(cuboid_volume(5.5),0)
AssertionError: 166.375 != 0 within 7 places (166.375 difference)

----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (failures=1)

Well, from the above output, you can observe that the last assert statement resulted in an AssertionError, hence a unit test failure. Python's unit test module shows you the reason for the failure, along with the number of failures your code has.

Now let's explore another assert method, i.e., assertRaises, which will help you in finding out whether your function cuboid_volume handles the input values correctly.

Let's say you want to test whether you function cuboid_volume handles the class or type of input, for example, if you pass a string as an input will it handle that input either as an exception or with an if condition since the length of the cuboid can never be a string.

!python -m unittest test_volume_cuboid.py
F.
======================================================================
FAIL: test_input_value (test_volume_cuboid.TestCuboid)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\Users\hda3kor\Documents\Unit_Testing_Python\test_volume_cuboid.py", line 17, in test_input_value
    self.assertRaises(TypeError, cuboid_volume, True)
AssertionError: TypeError not raised by cuboid_volume

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

FAILED (failures=1)

Great! So from the above output, it is evident that your code volume_cuboid.py doesn't take care of the input being passed to it properly.

Let's add a condition in the volume_cuboid.py to check whether the input or length of the cuboid is a boolean or a string and raise an error.

!python -m unittest test_volume_cuboid.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK

There are a lot of assert methods in the unit test module of Python, which could be leveraged for your testing purposes.

Below are some of the assert methods that exist in the unit test module.

Source

To know in detail about the assert methods, check out Python's official documentation.

You could also learn about them with the help of the Pydoc module, which is similar to a help function in Python. A demonstration of how you could use the Pydoc module to read about the assertCountEqual method's documentation is shown below.

!python -m pydoc unittest.TestCase.assertCountEqual
Help on function assertCountEqual in unittest.TestCase:

unittest.TestCase.assertCountEqual = assertCountEqual(self, first, second, msg=None)
    An unordered sequence comparison asserting that the same elements,
    regardless of order.  If the same element occurs more than once,
    it verifies that the elements occur the same number of times.

        self.assertEqual(Counter(list(first)),
                         Counter(list(second)))

     Example:
        - [0, 1, 1] and [1, 0, 1] compare equal.
        - [0, 0, 1] and [0, 1] compare unequal.

It is important to know that your test methods inside the Class TestCuboid should start with a keyword test. Let's see what happens when you start your test method with some random name without using the test naming convention.

!python -m unittest test_volume_cuboid.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

As you can see by changing the test_volume method name to volume, only 1 test was run, i.e., test_input_value. Hence, it is important to name your testing methods with a test keyword at the beginning.

Let's now run the testing script within the jupyter lab. To achieve this, you would first define the testing script in the jupyter lab, as shown below.

# -*- coding: utf-8 -*-
"""
Created on Sat Apr 25 20:16:58 2020

@author: Aditya
"""
from volume_cuboid import *
import unittest

class TestCuboid(unittest.TestCase):
    def test_volume(self):
        self.assertAlmostEqual(cuboid_volume(2),8)
        self.assertAlmostEqual(cuboid_volume(1),1)
        self.assertAlmostEqual(cuboid_volume(0),1)

    def test_input_value(self):
        self.assertRaises(TypeError, cuboid_volume, True)

Then, you will use the unittest.main() method to run the testing script, you can pass several arguments to the below method, out of which one is verbosity level.

Let's experiment with different verbosity levels and see how it changes the output description.

unittest.main(argv=[''],verbosity=0, exit=False)
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK





<unittest.main.TestProgram at 0x1de02774348>
unittest.main(argv=[''],verbosity=1, exit=False)
..
----------------------------------------------------------------------
Ran 2 tests in 0.002s

OK





<unittest.main.TestProgram at 0x1de027a1cc8>
unittest.main(argv=[''],verbosity=2, exit=False)
test_input_value (__main__.TestCuboid) ... ok
test_volume (__main__.TestCuboid) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.002s

OK





<unittest.main.TestProgram at 0x1de027a8308>

Lastly, let's modify the final assertAlmostEqual in the test_volume method and analyze how the verbosity level 2 will show the method failure.

# -*- coding: utf-8 -*-
"""
Created on Sat Apr 25 20:16:58 2020

@author: Aditya
"""
from volume_cuboid import *
import unittest

class TestCuboid(unittest.TestCase):
    def test_volume(self):
        self.assertAlmostEqual(cuboid_volume(2),8)
        self.assertAlmostEqual(cuboid_volume(1),1)
        self.assertAlmostEqual(cuboid_volume(0),1)

    def test_input_value(self):
        self.assertRaises(TypeError, cuboid_volume, True)
unittest.main(argv=[''],verbosity=2, exit=False)
test_input_value (__main__.TestCuboid) ... ok
test_volume (__main__.TestCuboid) ... FAIL

======================================================================
FAIL: test_volume (__main__.TestCuboid)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<ipython-input-28-46ad33b909ee>", line 14, in test_volume
    self.assertAlmostEqual(cuboid_volume(0),1)
AssertionError: 0 != 1 within 7 places (1 difference)

----------------------------------------------------------------------
Ran 2 tests in 0.003s

FAILED (failures=1)





<unittest.main.TestProgram at 0x1de027b5448>

From the above outputs, the following conclusions can be made:

  • Verbosity level 0 just shows how many tests were run, i.e., 2 tests along with the time it took to run them,
  • Verbosity level 1 add two dots .. which signifies two tests were executed,
  • Verbosity level 2 shows the detailed method names which were run along with their state ok or FAIL.

Conclusion

Congratulations on finishing the tutorial.

This tutorial was a basic introduction to unit testing in python and its importance as a developer. One good exercise for you would be to write a unit test module for any of your past projects. This will give you a good hands-on experience with writing unit tests. Also, try exploring the remaining Assert methods.

If you are just getting started in Python and would like to learn more, take DataCamp's Introduction to Data Science in Python course.