unittest
Unit Testing Tool
unittest
Overview
unittest is a unit testing framework included in Python's standard library. Inspired by JUnit for Java, it's an xUnit-style testing framework that supports test-driven development (TDD) and test-first development approaches. Since it's available immediately upon Python installation, you can start testing without any additional installations.
Details
unittest is built into Python's standard library, allowing you to build reliable testing environments without adding new dependencies. It adopts an object-oriented approach where test cases are created by inheriting from the unittest.TestCase base class.
Key features of unittest:
- Standard library: Included with Python, no additional installation required
- xUnit architecture: Design based on established testing patterns
- Object-oriented: Class-based test structure
- Rich assertions: Provides various verification methods
- Test suites: Supports test organization and batch execution
- Mock functionality: Test double creation through unittest.mock
- Test discovery: Automatic test detection feature
- Setup and teardown: Define pre and post-test processing
- Skip functionality: Conditionally skip tests
- CI/CD integration: Integration with Jenkins, GitHub Actions, etc.
unittest consists of four main concepts: test cases, test suites, test fixtures, and test runners. These concepts enable the construction of systematic and maintainable test structures.
Advantages and Disadvantages
Advantages
- No installation required: No additional setup needed as part of Python standard library
- Reliability: Stable framework used for many years
- Fast execution: Lightweight and quick test execution
- Rich assertions: Provides various verification methods as standard
- IDE integration: Supported by major IDEs like VS Code and PyCharm
- Test automation: Automatic test detection through test discovery
- Flexible test configuration: Organized test management through test suites
- Mock functionality: Supports dependency isolation and test double creation
Disadvantages
- Verbose syntax: More verbose compared to other frameworks (like pytest)
- Readability issues: Object-oriented abstraction can sometimes obscure intent
- Large project limitations: May feel inadequate for complex projects
- Limited parameterized testing: Limited support for data-driven tests
- Error messages: Less detailed error messages compared to pytest
- Plugin ecosystem: Not as rich plugin ecosystem as pytest
Reference Pages
- Official Documentation
- Python Testing 101
- Real Python unittest Guide
- Testing in Python Guide
- unittest.mock Documentation
Usage Examples
Basic Setup
Basic Test Class
import unittest
class TestStringMethods(unittest.TestCase):
def setUp(self):
"""Called before each test method execution"""
self.test_string = "hello world"
def tearDown(self):
"""Called after each test method execution"""
# Cleanup processing
pass
def test_upper(self):
"""Test string uppercase conversion"""
self.assertEqual(self.test_string.upper(), 'HELLO WORLD')
def test_split(self):
"""Test string splitting"""
result = self.test_string.split()
self.assertEqual(result, ['hello', 'world'])
self.assertEqual(len(result), 2)
if __name__ == '__main__':
unittest.main()
Running Tests
# File-level execution
python test_string_methods.py
# Module-level execution
python -m unittest test_string_methods
# Specific test class execution
python -m unittest test_string_methods.TestStringMethods
# Specific test method execution
python -m unittest test_string_methods.TestStringMethods.test_upper
# Test discovery (automatic detection)
python -m unittest discover
Assertion Methods
Basic Assertions
import unittest
class TestAssertions(unittest.TestCase):
def test_equality_assertions(self):
"""Equality assertions"""
self.assertEqual(1 + 1, 2)
self.assertNotEqual(1 + 1, 3)
def test_boolean_assertions(self):
"""Boolean assertions"""
self.assertTrue(True)
self.assertFalse(False)
def test_none_assertions(self):
"""None value assertions"""
value = None
self.assertIsNone(value)
value = "not none"
self.assertIsNotNone(value)
def test_membership_assertions(self):
"""Membership assertions"""
container = [1, 2, 3, 4, 5]
self.assertIn(3, container)
self.assertNotIn(6, container)
def test_type_assertions(self):
"""Type assertions"""
value = "hello"
self.assertIsInstance(value, str)
self.assertNotIsInstance(value, int)
Numeric and String Assertions
class TestNumericAndStringAssertions(unittest.TestCase):
def test_numeric_assertions(self):
"""Numeric assertions"""
self.assertGreater(5, 3)
self.assertGreaterEqual(5, 5)
self.assertLess(3, 5)
self.assertLessEqual(3, 3)
# Floating point approximate comparison
self.assertAlmostEqual(3.14159, 3.14, places=2)
self.assertNotAlmostEqual(3.14159, 2.71, places=2)
def test_string_assertions(self):
"""String assertions"""
text = "Hello, World!"
self.assertRegex(text, r'Hello.*')
self.assertNotRegex(text, r'goodbye')
def test_collection_assertions(self):
"""Collection assertions"""
list1 = [1, 2, 3]
list2 = [1, 2, 3]
self.assertListEqual(list1, list2)
dict1 = {'a': 1, 'b': 2}
dict2 = {'a': 1, 'b': 2}
self.assertDictEqual(dict1, dict2)
Exception Testing
Testing Exceptions
class TestExceptions(unittest.TestCase):
def test_exception_raised(self):
"""Test that an exception is raised"""
with self.assertRaises(ValueError):
int("not a number")
def test_exception_with_message(self):
"""Test exception with specific message"""
with self.assertRaises(ValueError) as context:
raise ValueError("Invalid value provided")
self.assertIn("Invalid value", str(context.exception))
def test_exception_not_raised(self):
"""Test that no exception is raised"""
try:
result = int("123")
self.assertEqual(result, 123)
except ValueError:
self.fail("ValueError was raised unexpectedly")
Setup and Teardown
Class-level Setup
class TestDatabaseOperations(unittest.TestCase):
@classmethod
def setUpClass(cls):
"""Class-wide setup (executed once)"""
print("Setting up database connection")
cls.db_connection = create_test_database()
@classmethod
def tearDownClass(cls):
"""Class-wide teardown (executed once)"""
print("Closing database connection")
cls.db_connection.close()
def setUp(self):
"""Setup before each test method"""
self.test_data = {"name": "John", "age": 30}
def tearDown(self):
"""Teardown after each test method"""
# Test data cleanup
self.db_connection.rollback()
def test_insert_user(self):
"""Test user insertion"""
result = self.db_connection.insert_user(self.test_data)
self.assertTrue(result)
Mocking
Using unittest.mock
import unittest
from unittest.mock import Mock, patch, MagicMock
class TestMocking(unittest.TestCase):
def test_mock_object(self):
"""Test mock object"""
mock_service = Mock()
mock_service.get_user.return_value = {"id": 1, "name": "John"}
result = mock_service.get_user(1)
self.assertEqual(result["name"], "John")
mock_service.get_user.assert_called_once_with(1)
@patch('requests.get')
def test_api_call(self, mock_get):
"""Mock external API call"""
# Set up mock response
mock_response = Mock()
mock_response.json.return_value = {"status": "success"}
mock_response.status_code = 200
mock_get.return_value = mock_response
# Call the function under test
response = make_api_call("http://example.com/api")
self.assertEqual(response["status"], "success")
mock_get.assert_called_once_with("http://example.com/api")
def test_side_effect(self):
"""Mock with side effects"""
mock_function = Mock()
mock_function.side_effect = [1, 2, 3, StopIteration]
self.assertEqual(mock_function(), 1)
self.assertEqual(mock_function(), 2)
self.assertEqual(mock_function(), 3)
with self.assertRaises(StopIteration):
mock_function()
Using Patch Decorator
class TestPatchDecorator(unittest.TestCase):
@patch('os.path.exists')
@patch('builtins.open', new_callable=unittest.mock.mock_open,
read_data="file content")
def test_file_operations(self, mock_open, mock_exists):
"""Mock file operations"""
mock_exists.return_value = True
result = read_file_content("test.txt")
self.assertEqual(result, "file content")
mock_exists.assert_called_once_with("test.txt")
mock_open.assert_called_once_with("test.txt", 'r')
Test Suites
Custom Test Suite
def create_test_suite():
"""Create custom test suite"""
suite = unittest.TestSuite()
# Add specific test cases
suite.addTest(TestStringMethods('test_upper'))
suite.addTest(TestStringMethods('test_split'))
# Add entire test class
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestAssertions))
return suite
if __name__ == '__main__':
# Run custom suite
runner = unittest.TextTestRunner(verbosity=2)
result = runner.run(create_test_suite())
Test Skipping
Conditional Skipping
import sys
import unittest
class TestSkipping(unittest.TestCase):
@unittest.skip("Feature not yet implemented")
def test_not_implemented_feature(self):
"""Test for unimplemented feature"""
pass
@unittest.skipIf(sys.version_info < (3, 8), "Requires Python 3.8+")
def test_python38_feature(self):
"""Test Python 3.8+ feature"""
# walrus operator (Python 3.8+)
if (n := len([1, 2, 3])) > 2:
self.assertGreater(n, 2)
@unittest.skipUnless(sys.platform.startswith("win"), "Windows only")
def test_windows_specific(self):
"""Test Windows-specific feature"""
import winsound
# Test Windows-specific processing
pass
def test_conditional_skip(self):
"""Runtime conditional skip"""
if not has_network_connection():
self.skipTest("Network connection required")
# Test using network
response = requests.get("http://example.com")
self.assertEqual(response.status_code, 200)
Parameterized Testing
Using Subtests
class TestParameterized(unittest.TestCase):
def test_multiple_inputs(self):
"""Test with multiple input values"""
test_cases = [
(2, 3, 5),
(1, 1, 2),
(0, 5, 5),
(-1, 1, 0)
]
for a, b, expected in test_cases:
with self.subTest(a=a, b=b, expected=expected):
result = add_numbers(a, b)
self.assertEqual(result, expected)
def test_string_operations(self):
"""Parameterized test for string operations"""
operations = {
"hello": "HELLO",
"world": "WORLD",
"python": "PYTHON"
}
for input_str, expected in operations.items():
with self.subTest(input=input_str):
result = input_str.upper()
self.assertEqual(result, expected)
Custom Assertions
Custom Assertion Methods
class CustomAssertionsMixin:
"""Custom assertions mixin"""
def assertBetween(self, value, min_val, max_val, msg=None):
"""Assert that value is between min and max"""
if not (min_val <= value <= max_val):
standardMsg = f'{value} not between {min_val} and {max_val}'
self.fail(self._formatMessage(msg, standardMsg))
def assertDictContainsSubset(self, subset, dictionary, msg=None):
"""Assert that dictionary contains subset"""
missing = []
mismatched = []
for key, value in subset.items():
if key not in dictionary:
missing.append(key)
elif dictionary[key] != value:
mismatched.append(f'{key}: expected {value}, got {dictionary[key]}')
if missing or mismatched:
errors = []
if missing:
errors.append(f'Missing keys: {missing}')
if mismatched:
errors.append(f'Mismatched values: {mismatched}')
standardMsg = '; '.join(errors)
self.fail(self._formatMessage(msg, standardMsg))
class TestCustomAssertions(unittest.TestCase, CustomAssertionsMixin):
def test_custom_assertions(self):
"""Test custom assertions"""
self.assertBetween(5, 1, 10)
full_dict = {"a": 1, "b": 2, "c": 3}
subset = {"a": 1, "b": 2}
self.assertDictContainsSubset(subset, full_dict)
Test Configuration and Coverage
Configuration File and Coverage Measurement
# conftest.py or test_config.py
import unittest
import coverage
import sys
class TestRunner:
def __init__(self):
self.cov = coverage.Coverage()
def run_tests_with_coverage(self):
"""Run tests with coverage measurement"""
self.cov.start()
# Run tests
loader = unittest.TestLoader()
suite = loader.discover('tests', pattern='test_*.py')
runner = unittest.TextTestRunner(verbosity=2)
result = runner.run(suite)
self.cov.stop()
self.cov.save()
print("\nCoverage Report:")
self.cov.report()
self.cov.html_report(directory='htmlcov')
return result.wasSuccessful()
if __name__ == '__main__':
runner = TestRunner()
success = runner.run_tests_with_coverage()
sys.exit(0 if success else 1)