In this post, we will see how to manage Unit Testing in our Python projects.
I just want to remember that Unit Testing is the process of testing individual components (units) of our code to verify that each part functions as intended. By isolating and testing the smallest parts of our application, we can catch bugs early, simplify debugging, and improve code quality.
Some benefits of Unit Testing are:
- Early Bug Detection: It allows us to identify and fix issues before they escalate into larger problems.
- Simplified Refactoring: With a suite of tests, we can modify code with confidence, knowing that tests will catch regressions.
- Documentation: Tests serve as live documentation of expected behaviour, making it easier to understand and maintain code.
- Improved Design: Writing testable code often leads to better, more modular architecture.
Python comes with a built-in unit testing framework called unittest, which provides tools for constructing and running tests.
Let’s see how to use it with a simple examples:
TESTING A FUNCTION:
In this example, we will create a Calculator class with an add method and then, we’ll write unit tests to verify that the add method returns correct results for various inputs.
[calculator.py]
class Calculator:
def add(self, input_a, input_b):
return input_a + input_b
[test_calculator.py]
import unittest
from calculator import Calculator
class TestCalculator(unittest.TestCase):
"""
This class contains unit tests for the Calculator class.
It inherits from unittest.TestCase, which provides testing capabilities.
"""
def test_add(self):
obj_calc = Calculator() # Create an instance of the Calculator class
# Test case 1: Adding two positive integers
self.assertEqual(obj_calc.add(3, 5), 8) # Expect 3 + 5 to equal 8
# Test case 2: Adding a negative and a positive integer
self.assertEqual(obj_calc.add(-1, 1), 0) # Expect -1 + 1 to equal 0
# Test case 3: Adding two negative integers
self.assertEqual(obj_calc.add(-1, -1), -2) # Expect -1 + (-1) to equal -2
if __name__ == '__main__':
# This block ensures that the test runs only when the script is executed directly,
# and not when it is imported as a module in another script.
unittest.main() # Run all test methods in the TestCalculator class

Obviously, if an unit test fails, we will receive a warning:
self.assertEqual(obj_calc.add(-1,10), -2)

TESTING EXCEPTION HANDLING:
In this example, we’ll extend the Calculator class with a divide method that includes exception handling for division by zero and then, we’ll write tests to cover both normal division operations and the exception case.
[calculator.py]
class Calculator:
def divide(self, input_a, input_b):
if input_b == 0:
raise ValueError("Cannot divide by zero")
return input_a/input_b
[test_calculator.py]
import unittest
from calculator import Calculator
class TestCalculator(unittest.TestCase):
"""
This class contains unit tests for the Calculator class.
It inherits from unittest.TestCase, which provides testing capabilities.
"""
def test_divide(self):
obj_calc = Calculator() # Create an instance of the Calculator class
# Test case 1: Dividing two positive integers
self.assertEqual(obj_calc.divide(10, 2), 5) # Expect 10 / 2 to equal 5
# Test case 2: Dividing a negative integer by a positive integer
self.assertEqual(obj_calc.divide(-10, 2), -5) # Expect -10 / 2 to equal -5
# Test case 3: Dividing by zero should raise a ValueError
with self.assertRaises(ValueError):
obj_calc.divide(10, 0) # Attempting to divide by zero should raise ValueError
if __name__ == '__main__':
# This block ensures that the test runs only when the script is executed directly,
# and not when it is imported as a module in another script.
unittest.main() # Run all test methods in the TestCalculator class

MOCKING EXTERNAL DEPENDENCIES:
In this last example, we’ll simulate an external logging service in the Calculator class.
We’ll use the unittest.mock module to mock the external service, ensuring our unit tests remain isolated and do not depend on external systems.
[calculator.py]
import requests
class Calculator:
def log_operation(self, operation, result):
response = requests.post('http://logging-service.com/log', json={
'operation': operation,
'result': result
})
return response.status_code == 200
[test_calculator.py]
import unittest
from calculator import Calculator
from unittest.mock import patch # Import patch for mocking external dependencies
class TestCalculator(unittest.TestCase):
"""
This class contains unit tests for the Calculator class.
It inherits from unittest.TestCase, which provides testing capabilities.
"""
# This decorator replaces the requests.post method in the calculator module with a mock during this test.
@patch('calculator.requests.post')
def test_log_operation(self, mock_post):
"""
Test the log_operation method of the Calculator class.
This method checks if the operation is correctly logged to an external service.
We use mocking to simulate the external HTTP request.
"""
# Configure the mock to return a response with status_code 200
mock_post.return_value.status_code = 200
calc = Calculator() # Create an instance of the Calculator class
result = calc.log_operation('add', 5) # Call the log_operation method
# Assert that the result is True (i.e., the logging was successful)
self.assertTrue(result)
# Assert that the requests.post method was called with the correct arguments
mock_post.assert_called_with('http://logging-service.com/log', json={
'operation': 'add',
'result': 5
})
if __name__ == '__main__':
# This block ensures that the test runs only when the script is executed directly,
# and not when it is imported as a module in another script.
unittest.main() # Run all test methods in the TestCalculator class

[CALCULATOR.PY]
import requests
class Calculator:
def add(self, input_a, input_b):
return input_a + input_b
def divide(self, input_a, input_b):
if input_b == 0:
raise ValueError("Cannot divide by zero")
return input_a/input_b
def log_operation(self, operation, result):
response = requests.post('http://logging-service.com/log', json={
'operation': operation,
'result': result
})
return response.status_code == 200
[TEST_CALCULATOR.PY]
import unittest
from calculator import Calculator
from unittest.mock import patch
class TestCalculator(unittest.TestCase):
def test_add(self):
obj_calc = Calculator()
self.assertEqual(obj_calc.add(3,5), 8)
self.assertEqual(obj_calc.add(-1,1), 0)
self.assertEqual(obj_calc.add(-1,-1), -2)
def test_divide(self):
obj_calc = Calculator()
self.assertEqual(obj_calc.divide(10, 2), 5)
self.assertEqual(obj_calc.divide(-10, 2), -5)
with self.assertRaises(ValueError):
obj_calc.divide(10, 0)
@patch('calculator.requests.post')
def test_log_operation(self, mock_post):
# Configure the mock to return a response with status_code 200
mock_post.return_value.status_code = 200
calc = Calculator()
result = calc.log_operation('add', 5)
self.assertTrue(result)
mock_post.assert_called_with('http://logging-service.com/log', json={
'operation': 'add',
'result': 5
})
if __name__ == '__main__':
unittest.main()