unittest

Pythonunit testingstandard libraryTDDxUnitTestCasemocktest suite

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

  1. No installation required: No additional setup needed as part of Python standard library
  2. Reliability: Stable framework used for many years
  3. Fast execution: Lightweight and quick test execution
  4. Rich assertions: Provides various verification methods as standard
  5. IDE integration: Supported by major IDEs like VS Code and PyCharm
  6. Test automation: Automatic test detection through test discovery
  7. Flexible test configuration: Organized test management through test suites
  8. Mock functionality: Supports dependency isolation and test double creation

Disadvantages

  1. Verbose syntax: More verbose compared to other frameworks (like pytest)
  2. Readability issues: Object-oriented abstraction can sometimes obscure intent
  3. Large project limitations: May feel inadequate for complex projects
  4. Limited parameterized testing: Limited support for data-driven tests
  5. Error messages: Less detailed error messages compared to pytest
  6. Plugin ecosystem: Not as rich plugin ecosystem as pytest

Reference Pages

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)