Enterprise Python Testing with unittest
Industrial-strength testing patterns with Python's built-in framework
Table of Contents
1. Test Cases (Industrial Strength Patterns)
Core Architecture
import unittest
class DatabaseTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
"""Class-level setup (runs once)"""
cls.engine = create_engine('sqlite:///:memory:')
Base.metadata.create_all(cls.engine)
def setUp(self):
"""Per-test setup (runs before each test)"""
self.connection = self.engine.connect()
self.transaction = self.connection.begin()
def tearDown(self):
"""Per-test cleanup (runs after each test)"""
self.transaction.rollback()
self.connection.close()
def test_record_creation(self):
"""Test method (must start with 'test_')"""
record = self.connection.execute("INSERT INTO...")
self.assertGreater(record.lastrowid, 0)
Advanced Patterns
# Dynamic test generation
def load_tests(loader, standard_tests, pattern):
suite = unittest.TestSuite()
# Add standard tests
suite.addTests(standard_tests)
# Add dynamic tests
for scenario in load_scenarios():
test_case = create_test_case(scenario)
suite.addTest(test_case)
return suite
# Test mixin pattern
class LoggingMixin:
def assertLogsOutput(self, logger, level, message):
with self.assertLogs(logger, level) as cm:
# Test code that should log
self.assertIn(message, cm.output[0])
Enterprise Example
class TransactionalAPITest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.app = create_test_app()
cls.client = cls.app.test_client()
cls.db = create_test_db()
def setUp(self):
self.db.begin_transaction()
def tearDown(self):
self.db.rollback_transaction()
def test_api_endpoint(self):
response = self.client.post('/api/data', json={'key': 'value'})
self.assertEqual(response.status_code, 201)
self.assertIn('Location', response.headers)
2. Assertions (Production Validation Toolkit)
Core Assertions
Method | Equivalent To | Use Case |
---|---|---|
assertEqual(a, b) |
a == b |
Value equality |
assertTrue(x) |
bool(x) is True |
Boolean truth |
assertRaises(Exc, func) |
with pytest.raises(Exc) |
Exception testing |
assertIs(a, b) |
a is b |
Identity check |
Advanced Validation
# Floating point comparison
self.assertAlmostEqual(0.1 + 0.2, 0.3, places=7)
# Collection assertions
self.assertCountEqual(['a', 'b', 'c'], ['b', 'c', 'a']) # Unordered comparison
# Type validation
self.assertIsInstance(response.json(), dict)
# Regex matching
self.assertRegex(response.text, r'^Success: \d+ items')
Custom Assertions
def assertValidXML(self, xml_string):
"""Validate XML structure and schema"""
try:
etree.fromstring(xml_string)
schema.assertValid(xml_string)
except (etree.ParseError, etree.DocumentInvalid) as e:
raise self.failureException(f"Invalid XML: {str(e)}")
# Register custom assertion
unittest.TestCase.assertValidXML = assertValidXML
# Usage in tests
def test_api_response(self):
response = self.client.get('/data.xml')
self.assertValidXML(response.data)
3. Test Suites (Enterprise Orchestration)
Basic Suite Construction
def suite():
suite = unittest.TestSuite()
# Add individual test methods
suite.addTest(TestClass('test_method'))
# Add multiple tests
suite.addTests([
TestClass2('test_feature_a'),
TestClass2('test_feature_b')
])
return suite
if __name__ == '__main__':
runner = unittest.TextTestRunner()
runner.run(suite())
Advanced Composition
# Module-level test discovery
loader = unittest.TestLoader()
suite = loader.discover(start_dir='./tests', pattern='test_*.py')
# Hierarchical test suites
top_suite = unittest.TestSuite()
unit_suite = loader.loadTestsFromModule(test_unit)
integration_suite = loader.loadTestsFromModule(test_integration)
top_suite.addTests([unit_suite, integration_suite])
CI/CD Integration
python -m unittest discover -s ./tests -p 'test_*.py' |
Discover and run all tests |
python -m unittest tests/test_module.py |
Run specific test module |
python -m unittest --xml-report ./reports |
Generate JUnit XML reports |
python -m unittest TestClass.test_method |
Run specific test method |
4. Advanced unittest Features
Mocking Framework
from unittest.mock import patch, MagicMock
class APITest(unittest.TestCase):
@patch('requests.get')
def test_api_call(self, mock_get):
# Configure mock
mock_response = MagicMock()
mock_response.json.return_value = {'key': 'value'}
mock_get.return_value = mock_response
# Test code that uses requests.get
result = fetch_data()
# Assertions
mock_get.assert_called_once_with('https://api.example.com')
self.assertEqual(result, {'key': 'value'})
Performance Testing
class PerformanceTests(unittest.TestCase):
@unittest.skipUnless(os.getenv('PERF_TESTS'), 'Performance tests disabled')
def test_response_time(self):
"""Verify API response time SLA"""
start_time = time.perf_counter()
make_api_call()
duration = time.perf_counter() - start_time
self.assertLess(duration, 0.5) # 500ms SLA
self.assertWarns(ResourceWarning, make_expensive_call)
Enterprise Tip: Combine unittest with the subTest
context manager for data-driven tests without generating separate test cases.
5. Migration & Modernization
unittest → pytest Bridge
Feature | unittest | pytest |
---|---|---|
Fixtures | setUp/tearDown | @pytest.fixture |
Assertions | assert* methods | Plain assert |
Parametrization | subTest | @pytest.mark.parametrize |
Test Discovery | test_* methods | More flexible patterns |
When unittest Shines
- Legacy systems: Django, stdlib modules
- Strict xUnit needs: JUnit-compatible output
- Team constraints: Developers familiar with JUnit-style
- Corporate standards: Mandated testing frameworks
Migration Warning: While pytest can run unittest tests, some features like custom test loaders may need adaptation during migration.
6. Complete Enterprise Example
import unittest
import warnings
from decimal import Decimal
from unittest.mock import patch
class FinancialPortfolioTest(unittest.TestCase):
"""End-to-end test of portfolio management system"""
@classmethod
def setUpClass(cls):
# Load test data once for all tests
cls.test_data = load_large_dataset('portfolio_fixture.json')
# Connect to test database
cls.engine = create_engine(DB_URL)
cls.metadata.create_all(cls.engine)
@classmethod
def tearDownClass(cls):
# Clean up database
cls.metadata.drop_all(cls.engine)
cls.engine.dispose()
def setUp(self):
# Fresh transaction for each test
self.connection = self.engine.connect()
self.transaction = self.connection.begin()
# Create fresh portfolio instance
self.portfolio = Portfolio(self.connection)
self.portfolio.initialize(self.test_data)
def tearDown(self):
# Rollback transaction
self.transaction.rollback()
self.connection.close()
def test_portfolio_valuation(self):
"""Test valuation under market conditions"""
with patch('finance.market_data.fetch') as mock_fetch:
mock_fetch.return_value = {
'AAPL': Decimal('175.50'),
'MSFT': Decimal('250.75')
}
value = self.portfolio.current_value()
self.assertIsInstance(value, Decimal)
self.assertGreater(value, Decimal('10000'))
@unittest.skipUnless(HAS_NETWORK, "Requires network access")
def test_live_data_integration(self):
"""Integration test with live market data"""
with warnings.catch_warnings():
warnings.simplefilter("ignore", ResourceWarning)
value = self.portfolio.current_value()
self.assertIsInstance(value, Decimal)
def test_transaction_audit_log(self):
"""Verify audit trail for transactions"""
with self.assertLogs('finance.audit', level='INFO') as cm:
self.portfolio.execute_trade('AAPL', 10, 'BUY')
self.assertIn('Trade executed', cm.output[0])
self.assertIn('AAPL', cm.output[0])
def load_tests(loader, standard_tests, pattern):
"""Custom test loader for enterprise environment"""
suite = unittest.TestSuite()
# Add standard tests
suite.addTests(standard_tests)
# Add integration tests if enabled
if os.getenv('RUN_INTEGRATION_TESTS'):
suite.addTests(loader.discover('integration_tests'))
return suite
if __name__ == '__main__':
unittest.main(failfast=True)
Production Pattern: For large enterprise suites, combine unittest with concurrent.futures
for parallel test execution across multiple modules.