Ultimate Guide to Understanding Python Unit Testing with Unittest Framework
Table of contents
- Introduction to Unit Testing in Python
- Understanding the Python Unittest Framework
- Prerequisites
- Testing Programs Using Test Case
- Testing Programs Using setUp() and teardown()
- Understanding the Use of TestSuite and Testrunner
- Advanced Testing Techniques
- 5 Best practices and tips for effective unit testing using python unittest
- Conclusion
Introduction to Unit Testing in Python
When creating a program, it is important to ensure it works as it should. In large and complex programs, it may sometimes be difficult to find bugs in code after programming. This is where unit testing comes in.
Unit testing is a software development process where individual sections of the software are tested to ensure they work accurately. It does this by isolating the code into several sections and testing it for accurate performance.
One significant advantage of unit testing is that it helps developers spot bugs and errors that may be more difficult to find in the later stage of development.
In Python development, two major frameworks- unittest
and pytest
are used to carry out unit testing.
In this article, we will cover all you need to know about testing codes using unittest
in Python.
Understanding the Python Unittest Framework
Unittest
is a unit testing framework that makes the testing of Python programs possible. It provides a variety of tools that aids in constructing and running tests. Some important concepts in the unittest include text fixture, test case, test suite, and test runner.
Test fixtures deal with the preparation and cleanup actions necessary to run unit tests. Common test fixtures include setup()
which is called before every test method and is used for setting up any necessary resources or initial conditions and teardown()
which is called after every test method and is used for cleaning up any resources.
The Testcase is used to check individual units of a program. Test cases in unittest
are responsible for defining the test scenarios, including assertions to check the expected behavior of the code being tested. They can also utilize test fixtures to set up and tear down the required resources. Common assert methods used are expressed in the image below
A Testsuite is a collection of test cases, other test suites, or even a mixture of both that are grouped together for execution. It allows you to run multiple test cases at the same time and organize them based on their logical grouping. In unittest
, a test suite is typically created by subclassing unittest.TestSuite
or using the unittest.TestLoader
class.
The test runner is responsible for discovering and executing the tests defined in your test suite.
Prerequisites
To follow up on this tutorial, you will need
An IDE
Basic understanding of Python classes and functions
Import the unittest framework. Unittest framework is available on a majority of IDEs including VScode and Pycharm so all you need to do is import it into your work file and start writing your code.
Testing Programs Using Test Case
Let’s test a code that performs simple calculations. To do this, we need to open a new file called main.py
and write the code below.
#main.py
class Calculator:
def init(self, num1, num2):
self.num1 = num1
self.num2 = num2
def add(self):
return self.num1 + self.num2
def subtract(self):
return self.num1 - self.num2
def multiply(self):
return self.num1 * self.num2
A class called Calculator
is created to handle mathematical calculations. The init
method which serves as the constructor for the Calculator
class is created also and it takes num1
and num2
as parameters, representing the numbers on which calculations will be performed. It further initializes the instance variables self.num1
and self.num2
with the values passed as arguments. The class provides three methods: add()
for addition, subtract()
for subtraction, and multiply()
for multiplication. Each method takes no additional parameters and performs the respective operation using the instance variables self.num1
and self.num2
.
Moving forward, we write the test code using the unittest.Testcase module to check if the code above is correct.
To do this, in the same folder as your main.py
, create a new file called maintest.py
and input the code below.
#maintest.py
import unittest
from main import Calculator
class TestCalculator(unittest.TestCase):
def test_add(self):
calculator = Calculator(4, 2)
self.assertEqual(calculator.add(), 6, 'The addition is incorrect.')
def test_subtract(self):
calculator = Calculator(4, 2)
self.assertEqual(calculator.subtract(), 'The subtraction is incorrect')
def test_multiply(self):
calculator = Calculator(4, 2)
self.assertEqual(calculator.multiply(), 8, 'The multiplication is incorrect')
if __name__ == '__main__':
unittest.main()
This code tests the various functions defined in our main.py
. To carry out the test do the following:
Import the unittest framework
Link this current file to the
main.py
file by importingmain
andCalculator
classCreate the
TestCalculations
class and give it theunittest.Testcase
inheritance. Inside this class, you test each function.
It is important to note that each test method starts with the prefix test and contains assertions to verify the expected behavior of the Calculator methods. For example, the test_add()
method tests the add()
method of the Calculator class and asserts that the result is equal to 6 (the expected sum of 4 and 2). It also goes further to attach a failure message which will be displayed if an incorrect answer pops up.
- Finally, run the test by calling the
unittest.main()
.
This script outputs the result below
Complex Problems
Now, let us test more complex scripts. In this example, we create a new file called newmain.py
and we have a TodoList
class that manages a list of tasks. The class has methods to add tasks, remove tasks, and get the count of tasks in the list. The code below explains this concept better.
#newmain.py
class TodoList:
def __init__(self):
self.tasks = []
def add_task(self, task):
self.tasks.append(task)
def remove_task(self, task):
self.tasks.remove(task)
def get_task_count(self):
return len(self.tasks)
To test the code, create another file in the same folder as newmain.py
, and give it the name newtest.py
. Using the code below, test the code:
#newtest.py
import unittest
from newmain import TodoList
class TestTodoList(unittest.TestCase):
def test_add_task(self):
todo_list = TodoList()
task = "Buy groceries"
todo_list.add_task(task)
self.assertEqual(todo_list.get_task_count(), 1)
def test_remove_task(self):
todo_list = TodoList()
task = "Buy groceries"
todo_list.add_task(task)
todo_list.remove_task(task)
self.assertEqual(todo_list.get_task_count(), 0)
def test_get_task_count(self):
todo_list = TodoList()
self.assertEqual(todo_list.get_task_count(), 0)
task1 = "Buy groceries"
task2 = "Walk the dog"
todo_list.add_task(task1)
todo_list.add_task(task2)
self.assertEqual(todo_list.get_task_count(), 2)
if __name__ == '__main__':
unittest.main()
This will import the unittest framework, import the TodoList
class from the newtest.py
, and do the following:
Create a
TestTodoList
class that inherits from theunittest.TestCase
and represent a test case for theTodoList
class.Use the
test_add_task method
to create an instance ofTodoList
, add a task ("Buy groceries"), and asserts that the task count is equal to 1.Use the
test_remove_task
method to create an instance ofTodoList
, add a task ("Buy groceries"), removes the same task, and assert that the task count is equal to 0.Use the
test_get_task_count
method creates an instance ofTodoList
, asserts that the initial task count is 0, adds two tasks ("Buy groceries" and "Walk the dog"), and asserts that the task count is equal to 2.
The unittest.main()
function runs the tests and provides the test results as below:
If we do a little manipulation in the test_remove_task
method part by changing the task count to 1 as indicated below, we get an error and the error message attached to the line of code is printed out.
def test_remove_task(self):
todo_list = TodoList()
task = "Buy groceries"
todo_list.add_task(task)
todo_list.remove_task(task)
self.assertEqual(todo_list.get_task_count(), 1, 'You got it wrong')
The output printed out is given below:
Testing Programs Using setUp() and teardown()
In Python's unittest framework, setUp
and tearDown
are special methods that can be used to set up and tear down resources or perform common actions before and after each test method is executed.
Let us explore how to use the setup()
method to test the Todolist
sample code in our newmain.py
.
To do this, open a new file and name it test.py
.
In the file, write the code below.
#test.py
import unittest
from newmain import TodoList
class TestTodoList(unittest.TestCase):
def setUp(self):
self.todo_list = TodoList()
self.task1 = "Buy groceries"
self.task2 = "Walk the dog"
def test_add_task(self):
self.todo_list.add_task(self.task1)
self.assertEqual(self.todo_list.get_task_count(), 1)
def test_remove_task(self):
self.todo_list.add_task(self.task1)
self.todo_list.remove_task(self.task1)
self.assertEqual(self.todo_list.get_task_count(), 0)
def test_get_task_count(self):
self.assertEqual(self.todo_list.get_task_count(), 0)
self.todo_list.add_task(self.task1)
self.todo_list.add_task(self.task2)
self.assertEqual(self.todo_list.get_task_count(), 2)
if __name__ == '__main__':
unittest.main()
In the setUp
method executed above, before each test method we set up a new instance of the TodoList
class using self.todo_list = TodoList()
. We also define two task strings, self.task1
and self.task2
, which will be used in the tests.
The test_add_task
method tests the add_task
method of the TodoList
class. It adds self.task1
to the TodoList
using self.todo_list.add_task(self.task1)
and then asserts that the task count is 1 using self.assertEqual(self.todo_list.get_task_count(), 1)
.
The test_remove_task
method tests the remove_task
method of the TodoList class. It adds self.task1
to the TodoList
, removes it using self.todo_list.remove_task(self.task1)
, and then asserts that the task count is 0 using self.assertEqual(self.todo_list.get_task_count(), 0)
.
The test_get_task_count
method tests the get_task_count
method of the TodoList
class. It asserts that the initial task count is 0 by calling self.assertEqual(self.todo_list.get_task_count(), 0)
. Then, it adds the two tasks using self.todo_list.add_task(self.task1)
and self.todo_list.add_task(self.task2)
. Finally, it asserts that the task count is 2 by calling self.assertEqual(self.todo_list.get_task_count(), 2)
. The unittest.main()
function runs the tests and provides the test results as below:
Teardown Method
The teardown()
method is illustrated below:
import unittest
from newmain import TodoList
class TestTodoList(unittest.TestCase):
def setUp(self):
self.todo_list = TodoList()
self.task1 = "Buy groceries"
self.task2 = "Walk the dog"
def tearDown(self):
self.todo_list = None
self.task1 = None
self.task2 = None
def test_add_task(self):
self.todo_list.add_task(self.task1)
self.assertEqual(self.todo_list.get_task_count(), 1)
def test_remove_task(self):
self.todo_list.add_task(self.task1)
self.todo_list.remove_task(self.task1)
self.assertEqual(self.todo_list.get_task_count(), 0)
def test_get_task_count(self):
self.assertEqual(self.todo_list.get_task_count(), 0)
self.todo_list.add_task(self.task1)
self.todo_list.add_task(self.task2)
self.assertEqual(self.todo_list.get_task_count(), 2)
if __name__ == '__main__':
unittest.main()
Inside the tearDown
method, we reset the self.todo_list, self.task1
, and self.task2
variables to None. This ensures that any resources or states created during the test are properly cleaned up, providing a clean state for the next test.
By including the tearDown
method, we guarantee that each test method is executed in isolation and does not leave any side effects that could affect the accuracy of other tests.
Understanding the Use of TestSuite and Testrunner
A TestSuite
is a collection of test cases that can be executed together. Testsuite allows users to group related tests and run them as a cohesive unit. One amazing thing about it is that it gives users the room to create and customize test suites to suit their testing needs.
To further explain how Testsuite and Testrunner work, we will use the TodoList
code as implemented in our newmain.py
.
Open a new file in the same folder as our newmain.py
and give it the name testnew.py
.
Copy the code below into your file.
#testnew.py
import unittest
from newmain import TodoList
class TestTodoList(unittest.TestCase):
def setUp(self):
self.todo_list = TodoList()
self.task1 = "Buy groceries"
self.task2 = "Walk the dog"
def test_add_task(self):
self.todo_list.add_task(self.task1)
self.assertEqual(self.todo_list.get_task_count(), 1)
def test_remove_task(self):
self.todo_list.add_task(self.task1)
self.todo_list.remove_task(self.task1)
self.assertEqual(self.todo_list.get_task_count(), 0)
def test_get_task_count(self):
self.assertEqual(self.todo_list.get_task_count(), 0)
self.todo_list.add_task(self.task1)
self.todo_list.add_task(self.task2)
self.assertEqual(self.todo_list.get_task_count(), 2)
if __name__ == '__main__':
# Create a TestSuite using TestLoader and load the tests from TestTodoList
loader = unittest.TestLoader()
suite = unittest.TestSuite()
suite.addTests(loader.loadTestsFromTestCase(TestTodoList))
# Run the TestSuite
runner = unittest.TextTestRunner()
runner.run(suite)
The TestLoader
class is used to create the TestSuite
and load the tests from the TestTodoList
test case.
We first create a TestLoader
object named loader
using unittest.TestLoader()
. Then, we create a TestSuite
object named suite
and add the tests from the TestTodoList
test case using suite.addTests(loader.loadTestsFromTestCase(TestTodoList))
.
The loadTestsFromTestCase()
method from TestLoader
is used to discover and load all the test methods within the TestTodoList
test case.
Finally, the TestSuite
is executed using the TextTestRunner
, and the test results are displayed in the console.
By using TestLoader
and loadTestsFromTestCase()
, we can dynamically discover and load the tests from test case classes, providing a more flexible and future-proof to create test suites. The TextTestRunner
is one of the runners provided by unittest that displays test results in the console.
Advanced Testing Techniques
Aside from the use of Testcase
, Testfixtures
, Testsuites
, and Testrunner
, there are some more advanced ways to test your code. In this article, we will focus on two such methods which are:
Skipping and excluding decorators
Testing exceptions with
assertRaises()
andasseetWarns()
Skipping and Excluding Decorators
In unittest
, skipping and excluding specific tests can be achieved using decorators. Decorators are special functions that modify the behavior of the decorated function or method. Two commonly used decorators for skipping and excluding tests in unittest
are @unittest.skip()
and @unittest.skipIf()
.
1. Skipping a test with @unittest.skip()
decorator:
This decorator is used to skip specific test cases or test methods and it marks the test as skipped and skips its execution when the test suite is run.
Here's an example:
import unittest
class MyTestCase(unittest.TestCase):
@unittest.skip("Enter a reason for skipping this test")
def test_something(self):
# Test implementation
pass
In this example, the test_something()
method is marked with @unittest.skip()
decorator, along with a reason for skipping. When the test suite is executed, this test will be skipped, and its result will be reported as skipped.
2. Skipping a test conditionally with @unittest.skipIf()
decorator:
To skip a test based on a condition, the @unittest.skipIf()
decorator is used a it allows the specification of a condition and if the condition evaluates to True
, the test will be skipped.
Here's an example:
import unittest
class MyTestCase(unittest.TestCase):
@unittest.skipIf(condition, reason)
def test_something(self):
# Test implementation
pass
In this example, condition
is a Boolean expression or a function returning a Boolean value. If the condition
is True
, the test will be skipped, and its result will be reported as skipped. You can also provide a reason
to explain why the test is being skipped.
It is essential to understand that these decorators provide flexibility in controlling the execution of tests based on specific conditions or requirements. They can be useful in scenarios where certain tests need to be skipped temporarily or conditionally excluded based on the environment or configuration.
Testing exceptions with assertRaises() and asseetWarns()
Another more advanced way to test a program is through the use of assertRaises()
and assertWarns()
methods.
1. assertRaises()
method:
This method is used to test whether a specific exception is raised when executing a particular code block. It ensures that the code under test raises the expected exception.
To demonstrate its use, consider the code sample below:
import unittest
class Calculator:
def divide(self, a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
class TestCalculator(unittest.TestCase):
def test_divide_by_zero(self):
calculator = Calculator()
with self.assertRaises(ValueError):
calculator.divide(10, 0)
if __name__ == '__main__':
unittest.main()
The Calculator
class is defined, which contains a divide()
method that performs division and raises a ValueError
if the divisor is zero.
The
TestCalculator
class is aunittest.TestCase
subclass that includes thetest_divide_by_zero()
method.In the
test_divide_by_zero()
method, an instance of theCalculator
class is created, and thedivide()
method is called with the arguments 10 and 0.The
self.assertRaises(ValueError)
context manager is used to assert that aValueError
is raised when executing the specified code.If the expected exception is raised within the context manager, the test case passes; otherwise, it fails.
2. assertWarns()
method:
This method is used to test whether a specific warning is issued when executing a particular code block. It ensures that the code under test generates the expected warning.
To demonstrate its use, consider the code sample below:
import unittest
import warnings
class Calculator:
def square_root(self, x):
if x < 0:
warnings.warn("Square root of a negative number")
return x + 1
class TestCalculator(unittest.TestCase):
def test_negative_number_warning(self):
calculator = Calculator()
with self.assertWarns(UserWarning):
calculator.square_root(-9)
if __name__ == '__main__':
unittest.main()
In this example:
The
Calculator
class is defined with asquare_root()
method that calculates the square root of a number. If the input (x) is negative, a UserWarning is raised.The
TestCalculator
class is aunittest.TestCase
subclass that contains thetest_negative_number_warning()
method.In the
test_negative_number_warning()
method, an instance of theCalculator
class is created, and thesquare_root()
method is called with the argument -9.The
self.assertWarns(UserWarning)
context manager is used to assert that a UserWarning is raised when executing the specified code.If the expected warning is raised within the context manager, the test case passes; otherwise, it fails.
assertwarns()
is useful for ensuring that specific warnings are raised when executing code. It allows you to define the expected warning behavior and assert that the code generates the expected warning under specific conditions.
5 Best practices and tips for effective unit testing using python unittest
Here are five best practices and tips for effective unit testing using the unittest
framework in Python:
Keep Tests Independent and Isolated:
Always ensure that each unit test case is independent and isolated from other tests and does not depend on the outcome of other tests. This helps in identifying issues more easily and preventing any side effects thus ensuring that the tests remain reliable regardless of the order of execution.
Descriptive Test Names:
Use descriptive test names for every test method. This provides more clarity of intentions and makes it easier for anyone who stumbles on your code to understand the purpose of the test.
Validate actions using assertions:
Make use of assertions from the unittest.TestCase
class to validate the expected outcomes of your tests and always use appropriate assertions based on the type of test and the expected behavior.
Use setUp() and tearDown():
Utilize the setUp()
method to set up preconditions and create common objects required for multiple test methods in the same test case. Additionally, tearDown()
can be used to clean up any resources after each test. This approach avoids code duplication and ensures a clean environment for each test.
Organize Tests with Test Suites:
Group related tests into test suites using the TestSuite
class. Test suites allow you to run multiple test cases together and organize tests hierarchically. This is particularly useful when you have a large number of tests or want to group tests based on specific functionalities or modules.
Lastly, when using the unittest framework to test codes, always ensure you write clear and concise tests. Make your tests readable and maintainable. Two ways to achieve this is by using comments and docstrings to provide context and explanations when necessary.
By following these best practices and tips, you can write effective unit tests that are easy to understand, maintain, and debug, thereby improving the reliability and quality of your code.
Conclusion
The Python unittest framework is a good way to ensure that all lines of your code are working as expected. It is of great importance to test and run your code before deploying it.
Ensure to adopt the tips and best practices listed above for proper maintenance and debugging of your code.
Subscribe to my newsletter
Read articles from Marvellous Kalu directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Marvellous Kalu
Marvellous Kalu
I write about my software engineering projects and learning