Master unit testing, debugging techniques, and quality assurance practices using Python's unittest framework and debugging tools.
Testing and debugging are fundamental skills for reliable software development. This lesson covers how to write effective unit tests, use Python's debugging tools, implement test-driven development practices, and ensure code quality through comprehensive testing strategies.
Testing ensures code reliability and catches bugs early in the development process. Test-Driven Development (TDD) is a fundamental approach to building robust software.
Testing Types and Principles:
1import unittest
2
3# Example demonstrating different types of tests
4
5def add(a, b):
6 """Simple function to demonstrate testing"""
7 return a + b
8
9def divide(a, b):
10 """Function that can raise exceptions"""
11 if b == 0:
12 raise ValueError("Cannot divide by zero")
13 return a / b
14
15class Calculator:
16 """Class to demonstrate object testing"""
17 def __init__(self):
18 self.history = []
19
20 def calculate(self, operation, a, b):
21 if operation == "add":
22 result = add(a, b)
23 elif operation == "divide":
24 result = divide(a, b)
25 else:
26 raise ValueError("Unsupported operation")
27
28 self.history.append((operation, a, b, result))
29 return result
30
31# Unit Tests - Test individual functions
32class TestMathFunctions(unittest.TestCase):
33
34 def test_add_positive_numbers(self):
35 """Test adding positive numbers"""
36 self.assertEqual(add(2, 3), 5)
37 self.assertEqual(add(10, 20), 30)
38
39 def test_add_negative_numbers(self):
40 """Test adding negative numbers"""
41 self.assertEqual(add(-5, -3), -8)
42 self.assertEqual(add(-10, 5), -5)
43
44 def test_divide_valid_numbers(self):
45 """Test valid division"""
46 self.assertEqual(divide(10, 2), 5)
47 self.assertAlmostEqual(divide(7, 3), 2.333333, places=5)
48
49 def test_divide_by_zero(self):
50 """Test division by zero raises exception"""
51 with self.assertRaises(ValueError) as context:
52 divide(10, 0)
53 self.assertIn("Cannot divide by zero", str(context.exception))
54
55# Integration Tests - Test component interactions
56class TestCalculator(unittest.TestCase):
57
58 def setUp(self):
59 """Set up test fixtures before each test"""
60 self.calculator = Calculator()
61
62 def test_calculator_operations(self):
63 """Test calculator with various operations"""
64 # Test addition
65 result = self.calculator.calculate("add", 5, 3)
66 self.assertEqual(result, 8)
67
68 # Test division
69 result = self.calculator.calculate("divide", 15, 3)
70 self.assertEqual(result, 5)
71
72 # Check history tracking
73 self.assertEqual(len(self.calculator.history), 2)
74
75 def test_calculator_invalid_operation(self):
76 """Test calculator with invalid operation"""
77 with self.assertRaises(ValueError):
78 self.calculator.calculate("multiply", 5, 3)Test-Driven Development (TDD) Cycle:
1# TDD Example: Building a BankAccount class
2
3# STEP 1: Write failing test
4class TestBankAccount(unittest.TestCase):
5 def test_initial_balance(self):
6 account = BankAccount(100)
7 self.assertEqual(account.balance, 100)
8
9# STEP 2: Write minimal code to pass
10class BankAccount:
11 def __init__(self, initial_balance):
12 self.balance = initial_balance
13
14# STEP 3: Add more tests and refactor
15class TestBankAccountExpanded(unittest.TestCase):
16 def setUp(self):
17 self.account = BankAccount(1000)
18
19 def test_deposit(self):
20 self.account.deposit(200)
21 self.assertEqual(self.account.balance, 1200)
22
23 def test_withdraw_sufficient_funds(self):
24 self.account.withdraw(300)
25 self.assertEqual(self.account.balance, 700)
26
27 def test_withdraw_insufficient_funds(self):
28 with self.assertRaises(ValueError):
29 self.account.withdraw(1500)
30
31# Expanded implementation
32class BankAccount:
33 def __init__(self, initial_balance):
34 if initial_balance < 0:
35 raise ValueError("Initial balance cannot be negative")
36 self.balance = initial_balance
37
38 def deposit(self, amount):
39 if amount <= 0:
40 raise ValueError("Deposit amount must be positive")
41 self.balance += amount
42
43 def withdraw(self, amount):
44 if amount <= 0:
45 raise ValueError("Withdrawal amount must be positive")
46 if amount > self.balance:
47 raise ValueError("Insufficient funds")
48 self.balance -= amountTesting Best Practices:
Good tests serve as documentation, catch regressions, and enable confident refactoring.
The unittest framework provides a comprehensive testing infrastructure with TestCase classes, assertion methods, and test organization capabilities.
unittest Framework Fundamentals:
1import unittest
2from unittest.mock import Mock, patch, MagicMock
3import tempfile
4import os
5
6class TestUnittestFeatures(unittest.TestCase):
7 """Comprehensive demonstration of unittest features"""
8
9 @classmethod
10 def setUpClass(cls):
11 """Called once before all tests in the class"""
12 print("Setting up class-level resources")
13 cls.shared_resource = "Shared data"
14
15 @classmethod
16 def tearDownClass(cls):
17 """Called once after all tests in the class"""
18 print("Cleaning up class-level resources")
19
20 def setUp(self):
21 """Called before each test method"""
22 self.test_data = [1, 2, 3, 4, 5]
23 self.temp_file = tempfile.NamedTemporaryFile(delete=False)
24
25 def tearDown(self):
26 """Called after each test method"""
27 if os.path.exists(self.temp_file.name):
28 os.unlink(self.temp_file.name)
29
30 # Assertion Methods
31 def test_equality_assertions(self):
32 """Test various equality assertions"""
33 self.assertEqual(2 + 2, 4)
34 self.assertNotEqual(2 + 2, 5)
35 self.assertAlmostEqual(0.1 + 0.2, 0.3, places=7)
36
37 def test_boolean_assertions(self):
38 """Test boolean assertions"""
39 self.assertTrue(True)
40 self.assertFalse(False)
41 self.assertIs(None, None)
42 self.assertIsNot([], [])
43
44 def test_container_assertions(self):
45 """Test container-related assertions"""
46 self.assertIn(3, self.test_data)
47 self.assertNotIn(10, self.test_data)
48 self.assertListEqual([1, 2, 3], [1, 2, 3])
49 self.assertCountEqual([1, 2, 3], [3, 2, 1]) # Order doesn't matter
50
51 def test_exception_assertions(self):
52 """Test exception handling"""
53 with self.assertRaises(ZeroDivisionError):
54 1 / 0
55
56 with self.assertRaises(ValueError) as context:
57 int("not a number")
58
59 self.assertIn("invalid literal", str(context.exception))
60
61 def test_regex_assertions(self):
62 """Test regular expression assertions"""
63 text = "Hello, World!"
64 self.assertRegex(text, r"Hello.*World")
65 self.assertNotRegex(text, r"d+") # No digits
66
67 def test_type_assertions(self):
68 """Test type-related assertions"""
69 self.assertIsInstance("hello", str)
70 self.assertNotIsInstance(123, str)
71
72 def test_custom_assertion_messages(self):
73 """Test custom assertion messages"""
74 value = 42
75 self.assertEqual(value, 42, f"Expected 42, but got {value}")
76
77 # Skip and Expected Failure
78 @unittest.skip("Demonstrating skipping")
79 def test_skipped(self):
80 """This test will be skipped"""
81 self.fail("This should not run")
82
83 @unittest.skipIf(os.name == "nt", "Not supported on Windows")
84 def test_skip_conditional(self):
85 """Skip based on condition"""
86 self.assertTrue(True)
87
88 @unittest.expectedFailure
89 def test_expected_failure(self):
90 """This test is expected to fail"""
91 self.assertEqual(1, 2) # Will fail, but expected
92
93 # Subtests for parameterized testing
94 def test_subtests(self):
95 """Demonstrate subtests for parameterized testing"""
96 test_cases = [
97 (2, 3, 5),
98 (1, 1, 2),
99 (0, 5, 5),
100 (-1, 1, 0)
101 ]
102
103 for a, b, expected in test_cases:
104 with self.subTest(a=a, b=b, expected=expected):
105 self.assertEqual(a + b, expected)
106
107class TestMockingExamples(unittest.TestCase):
108 """Examples of mocking in tests"""
109
110 def test_simple_mock(self):
111 """Basic mock usage"""
112 # Create a mock object
113 mock_obj = Mock()
114
115 # Configure mock behavior
116 mock_obj.method.return_value = "mocked result"
117 mock_obj.property = "mocked property"
118
119 # Use the mock
120 result = mock_obj.method()
121 self.assertEqual(result, "mocked result")
122
123 # Verify mock was called
124 mock_obj.method.assert_called_once()
125
126 def test_mock_with_side_effects(self):
127 """Mock with side effects"""
128 mock_obj = Mock()
129
130 # Side effect that raises exception
131 mock_obj.risky_method.side_effect = ValueError("Mocked error")
132
133 with self.assertRaises(ValueError):
134 mock_obj.risky_method()
135
136 # Side effect with sequence of values
137 mock_obj.counter.side_effect = [1, 2, 3]
138
139 self.assertEqual(mock_obj.counter(), 1)
140 self.assertEqual(mock_obj.counter(), 2)
141 self.assertEqual(mock_obj.counter(), 3)
142
143 @patch('builtins.open', new_callable=unittest.mock.mock_open, read_data="file content")
144 def test_patch_decorator(self, mock_file):
145 """Using patch decorator"""
146 # The open function is mocked
147 with open("fake_file.txt", "r") as f:
148 content = f.read()
149
150 self.assertEqual(content, "file content")
151 mock_file.assert_called_once_with("fake_file.txt", "r")
152
153 def test_patch_context_manager(self):
154 """Using patch as context manager"""
155 with patch('os.path.exists') as mock_exists:
156 mock_exists.return_value = True
157
158 # Now os.path.exists always returns True
159 self.assertTrue(os.path.exists("any_path"))
160 mock_exists.assert_called_with("any_path")
161
162 def test_spec_mocking(self):
163 """Mock with spec to ensure interface compliance"""
164 # Mock that enforces the interface of a list
165 mock_list = Mock(spec=list)
166
167 # These will work (list has these methods)
168 mock_list.append("item")
169 mock_list.pop()
170
171 # This would raise AttributeError if uncommented
172 # mock_list.nonexistent_method()
173
174class FileProcessor:
175 """Example class for testing"""
176
177 def __init__(self, file_path):
178 self.file_path = file_path
179
180 def process_file(self):
181 """Process a file (example with external dependency)"""
182 if not os.path.exists(self.file_path):
183 raise FileNotFoundError(f"File {self.file_path} not found")
184
185 with open(self.file_path, 'r') as f:
186 content = f.read()
187
188 return content.upper()
189
190class TestFileProcessor(unittest.TestCase):
191 """Test FileProcessor with mocking"""
192
193 @patch('os.path.exists')
194 @patch('builtins.open', new_callable=unittest.mock.mock_open, read_data="hello world")
195 def test_process_file_success(self, mock_open, mock_exists):
196 """Test successful file processing"""
197 mock_exists.return_value = True
198
199 processor = FileProcessor("test.txt")
200 result = processor.process_file()
201
202 self.assertEqual(result, "HELLO WORLD")
203 mock_exists.assert_called_once_with("test.txt")
204 mock_open.assert_called_once_with("test.txt", 'r')
205
206 @patch('os.path.exists')
207 def test_process_file_not_found(self, mock_exists):
208 """Test file not found scenario"""
209 mock_exists.return_value = False
210
211 processor = FileProcessor("missing.txt")
212
213 with self.assertRaises(FileNotFoundError):
214 processor.process_file()Test Organization and Discovery:
1# Test Suite Creation
2def create_test_suite():
3 """Create a custom test suite"""
4 suite = unittest.TestSuite()
5
6 # Add specific test cases
7 suite.addTest(TestUnittestFeatures('test_equality_assertions'))
8 suite.addTest(TestMockingExamples('test_simple_mock'))
9
10 # Add entire test classes
11 suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestFileProcessor))
12
13 return suite
14
15# Running tests programmatically
16if __name__ == '__main__':
17 # Run all tests in current module
18 unittest.main()
19
20 # Or run specific test suite
21 runner = unittest.TextTestRunner(verbosity=2)
22 suite = create_test_suite()
23 runner.run(suite)The unittest framework provides everything needed for comprehensive testing, from basic assertions to advanced mocking capabilities.
Effective debugging combines systematic thinking with proper tools. Python provides several debugging approaches for different scenarios.
Python Debugger (pdb):
1import pdb
2
3def buggy_function(numbers):
4 """Function with potential bugs for debugging demonstration"""
5 total = 0
6 for i, num in enumerate(numbers):
7 # Set breakpoint for debugging
8 pdb.set_trace() # Execution will pause here
9
10 if i % 2 == 0:
11 total += num * 2
12 else:
13 total += num
14
15 return total
16
17# Alternative debugging approaches
18def debug_with_post_mortem():
19 """Demonstrate post-mortem debugging"""
20 try:
21 result = 10 / 0 # This will cause an error
22 except:
23 pdb.post_mortem() # Drop into debugger at exception point
24
25# Using pdb in scripts
26if __name__ == "__main__":
27 # Start script in debugger
28 pdb.run('buggy_function([1, 2, 3, 4, 5])')
29
30# Common pdb commands:
31# n (next line)
32# s (step into function)
33# c (continue)
34# l (list current code)
35# p <variable> (print variable)
36# pp <variable> (pretty print)
37# w (where - show stack trace)
38# q (quit)Logging for Debugging:
1import logging
2from functools import wraps
3
4# Configure logging
5logging.basicConfig(
6 level=logging.DEBUG,
7 format='%(asctime)s - %(name)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s',
8 handlers=[
9 logging.FileHandler('debug.log'),
10 logging.StreamHandler()
11 ]
12)
13
14logger = logging.getLogger(__name__)
15
16def debug_decorator(func):
17 """Decorator to add debug logging to functions"""
18 @wraps(func)
19 def wrapper(*args, **kwargs):
20 logger.debug(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
21
22 try:
23 result = func(*args, **kwargs)
24 logger.debug(f"{func.__name__} returned: {result}")
25 return result
26 except Exception as e:
27 logger.error(f"{func.__name__} raised {type(e).__name__}: {e}")
28 raise
29
30 return wrapper
31
32class DatabaseConnection:
33 """Example class with comprehensive logging"""
34
35 def __init__(self, host, port):
36 self.host = host
37 self.port = port
38 self.connected = False
39 logger.info(f"DatabaseConnection created for {host}:{port}")
40
41 @debug_decorator
42 def connect(self):
43 """Simulate database connection"""
44 logger.debug(f"Attempting to connect to {self.host}:{self.port}")
45
46 # Simulate connection logic
47 if self.host == "localhost" and self.port == 5432:
48 self.connected = True
49 logger.info("Successfully connected to database")
50 return True
51 else:
52 logger.warning(f"Failed to connect to {self.host}:{self.port}")
53 return False
54
55 @debug_decorator
56 def execute_query(self, query):
57 """Execute database query with logging"""
58 if not self.connected:
59 logger.error("Cannot execute query: not connected")
60 raise ConnectionError("Database not connected")
61
62 logger.info(f"Executing query: {query[:50]}...") # Log first 50 chars
63
64 # Simulate query execution
65 if "SELECT" in query.upper():
66 result = [(1, "John"), (2, "Jane")]
67 logger.debug(f"Query returned {len(result)} rows")
68 return result
69 else:
70 logger.warning(f"Unsupported query type: {query}")
71 return None
72
73# Usage example
74def demonstrate_logging():
75 db = DatabaseConnection("localhost", 5432)
76 db.connect()
77 db.execute_query("SELECT * FROM users")Advanced Debugging Techniques:
1import sys
2import traceback
3import inspect
4from contextlib import contextmanager
5
6class DebugContext:
7 """Context manager for detailed debugging information"""
8
9 def __init__(self, name):
10 self.name = name
11 self.start_time = None
12
13 def __enter__(self):
14 import time
15 self.start_time = time.time()
16 print(f"[DEBUG] Entering {self.name}")
17
18 # Print current stack frame info
19 frame = inspect.currentframe().f_back
20 print(f"[DEBUG] Called from {frame.f_code.co_filename}:{frame.f_lineno}")
21 print(f"[DEBUG] Local variables: {list(frame.f_locals.keys())}")
22
23 return self
24
25 def __exit__(self, exc_type, exc_val, exc_tb):
26 import time
27 duration = time.time() - self.start_time
28
29 if exc_type:
30 print(f"[DEBUG] {self.name} failed after {duration:.4f}s")
31 print(f"[DEBUG] Exception: {exc_type.__name__}: {exc_val}")
32 self.print_detailed_traceback(exc_tb)
33 else:
34 print(f"[DEBUG] {self.name} completed successfully in {duration:.4f}s")
35
36 def print_detailed_traceback(self, tb):
37 """Print detailed traceback information"""
38 print("[DEBUG] Detailed traceback:")
39 for filename, lineno, name, line in traceback.extract_tb(tb):
40 print(f"[DEBUG] File {filename}, line {lineno}, in {name}")
41 print(f"[DEBUG] {line}")
42
43def debug_variable_state(*variables):
44 """Print detailed information about variables"""
45 frame = inspect.currentframe().f_back
46
47 for var_name in variables:
48 if var_name in frame.f_locals:
49 value = frame.f_locals[var_name]
50 print(f"[DEBUG] {var_name} = {repr(value)} (type: {type(value).__name__})")
51
52 # Additional info for collections
53 if hasattr(value, '__len__') and not isinstance(value, str):
54 print(f"[DEBUG] Length: {len(value)}")
55
56 if hasattr(value, '__dict__'):
57 print(f"[DEBUG] Attributes: {list(value.__dict__.keys())}")
58 else:
59 print(f"[DEBUG] {var_name} is not defined in local scope")
60
61class SmartAssert:
62 """Enhanced assertions with debugging information"""
63
64 @staticmethod
65 def assert_equal(actual, expected, message=""):
66 if actual != expected:
67 frame = inspect.currentframe().f_back
68 filename = frame.f_code.co_filename
69 lineno = frame.f_lineno
70
71 error_msg = f"""
72Assertion failed at {filename}:{lineno}
73Expected: {expected!r} (type: {type(expected).__name__})
74Actual: {actual!r} (type: {type(actual).__name__})
75Message: {message}
76
77Local variables:
78"""
79
80 for name, value in frame.f_locals.items():
81 if not name.startswith('_'):
82 error_msg += f" {name} = {repr(value)}
83"
84
85 raise AssertionError(error_msg)
86
87# Example usage of advanced debugging
88def complex_calculation(data):
89 """Function demonstrating advanced debugging techniques"""
90 with DebugContext("complex_calculation"):
91 debug_variable_state('data')
92
93 # Processing steps with debugging
94 filtered_data = [x for x in data if x > 0]
95 debug_variable_state('filtered_data')
96
97 if not filtered_data:
98 logger.warning("No positive numbers found in data")
99 return 0
100
101 # Calculate result
102 result = sum(filtered_data) / len(filtered_data)
103 debug_variable_state('result')
104
105 # Validate result
106 SmartAssert.assert_equal(type(result), float, "Result should be float")
107
108 return result
109
110# Memory debugging
111def debug_memory_usage():
112 """Monitor memory usage for debugging"""
113 import psutil
114 import os
115
116 process = psutil.Process(os.getpid())
117
118 def print_memory_info(label):
119 memory_info = process.memory_info()
120 print(f"[MEMORY] {label}: {memory_info.rss / 1024 / 1024:.2f} MB")
121
122 print_memory_info("Before operation")
123
124 # Simulate memory-intensive operation
125 large_list = [i for i in range(1000000)]
126
127 print_memory_info("After creating large list")
128
129 del large_list
130
131 print_memory_info("After deleting large list")
132
133# Exception debugging utilities
134def enhanced_exception_handler(exc_type, exc_value, exc_traceback):
135 """Enhanced exception handler for debugging"""
136 if issubclass(exc_type, KeyboardInterrupt):
137 sys.__excepthook__(exc_type, exc_value, exc_traceback)
138 return
139
140 print("
141" + "="*60)
142 print("UNHANDLED EXCEPTION DETECTED")
143 print("="*60)
144
145 print(f"Exception Type: {exc_type.__name__}")
146 print(f"Exception Message: {exc_value}")
147
148 print("
149Detailed Traceback:")
150 traceback.print_exception(exc_type, exc_value, exc_traceback)
151
152 print("
153Local Variables at Exception Point:")
154 frame = exc_traceback.tb_frame
155 for name, value in frame.f_locals.items():
156 try:
157 print(f" {name} = {repr(value)}")
158 except:
159 print(f" {name} = <could not represent>")
160
161 print("="*60)
162
163# Install the enhanced exception handler
164# sys.excepthook = enhanced_exception_handlerEffective debugging requires the right tools for each situation - from simple print statements to sophisticated debuggers and logging systems.
Code quality involves multiple dimensions including readability, maintainability, performance, and correctness. Testing is just one aspect of a comprehensive quality strategy.
Test Coverage Analysis:
1# Example using coverage.py (would need to be installed)
2# Command line: pip install coverage
3# Usage: coverage run --source=. test_module.py
4# coverage report
5# coverage html
6
7import unittest
8from unittest.mock import patch
9
10class ShoppingCart:
11 """Example class for coverage analysis"""
12
13 def __init__(self):
14 self.items = []
15 self.discount_rate = 0.0
16
17 def add_item(self, name, price, quantity=1):
18 """Add item to cart"""
19 if price < 0:
20 raise ValueError("Price cannot be negative")
21 if quantity <= 0:
22 raise ValueError("Quantity must be positive")
23
24 item = {
25 'name': name,
26 'price': price,
27 'quantity': quantity,
28 'total': price * quantity
29 }
30 self.items.append(item)
31 return item
32
33 def remove_item(self, name):
34 """Remove item from cart"""
35 for i, item in enumerate(self.items):
36 if item['name'] == name:
37 return self.items.pop(i)
38 return None
39
40 def apply_discount(self, rate):
41 """Apply discount to entire cart"""
42 if not 0 <= rate <= 1:
43 raise ValueError("Discount rate must be between 0 and 1")
44 self.discount_rate = rate
45
46 def get_total(self):
47 """Calculate total cart value"""
48 subtotal = sum(item['total'] for item in self.items)
49 discount_amount = subtotal * self.discount_rate
50 return subtotal - discount_amount
51
52 def get_item_count(self):
53 """Get total number of items"""
54 return sum(item['quantity'] for item in self.items)
55
56 def is_empty(self):
57 """Check if cart is empty"""
58 return len(self.items) == 0
59
60 def clear(self):
61 """Clear all items from cart"""
62 self.items.clear()
63 self.discount_rate = 0.0
64
65 def apply_bulk_discount(self):
66 """Apply bulk discount for large orders"""
67 total_items = self.get_item_count()
68
69 if total_items >= 100: # This line might not be covered
70 self.apply_discount(0.15) # 15% discount
71 elif total_items >= 50:
72 self.apply_discount(0.10) # 10% discount
73 elif total_items >= 20:
74 self.apply_discount(0.05) # 5% discount
75 # No discount for less than 20 items
76
77class TestShoppingCartCoverage(unittest.TestCase):
78 """Test suite demonstrating different levels of coverage"""
79
80 def setUp(self):
81 self.cart = ShoppingCart()
82
83 def test_add_item_valid(self):
84 """Test adding valid items"""
85 item = self.cart.add_item("Apple", 1.50, 3)
86
87 self.assertEqual(item['name'], "Apple")
88 self.assertEqual(item['price'], 1.50)
89 self.assertEqual(item['quantity'], 3)
90 self.assertEqual(item['total'], 4.50)
91 self.assertEqual(len(self.cart.items), 1)
92
93 def test_add_item_invalid_price(self):
94 """Test adding item with negative price"""
95 with self.assertRaises(ValueError):
96 self.cart.add_item("Invalid", -1.0)
97
98 def test_add_item_invalid_quantity(self):
99 """Test adding item with invalid quantity"""
100 with self.assertRaises(ValueError):
101 self.cart.add_item("Invalid", 1.0, 0)
102
103 with self.assertRaises(ValueError):
104 self.cart.add_item("Invalid", 1.0, -1)
105
106 def test_remove_existing_item(self):
107 """Test removing existing item"""
108 self.cart.add_item("Apple", 1.50, 2)
109 removed_item = self.cart.remove_item("Apple")
110
111 self.assertIsNotNone(removed_item)
112 self.assertEqual(removed_item['name'], "Apple")
113 self.assertTrue(self.cart.is_empty())
114
115 def test_remove_nonexistent_item(self):
116 """Test removing non-existent item"""
117 self.cart.add_item("Apple", 1.50, 2)
118 removed_item = self.cart.remove_item("Orange")
119
120 self.assertIsNone(removed_item)
121 self.assertFalse(self.cart.is_empty())
122
123 def test_apply_valid_discount(self):
124 """Test applying valid discount"""
125 self.cart.add_item("Apple", 10.0, 1)
126 self.cart.apply_discount(0.1) # 10% discount
127
128 self.assertEqual(self.cart.get_total(), 9.0)
129
130 def test_apply_invalid_discount(self):
131 """Test applying invalid discount rates"""
132 with self.assertRaises(ValueError):
133 self.cart.apply_discount(-0.1) # Negative discount
134
135 with self.assertRaises(ValueError):
136 self.cart.apply_discount(1.5) # > 100% discount
137
138 def test_get_total_empty_cart(self):
139 """Test total calculation for empty cart"""
140 self.assertEqual(self.cart.get_total(), 0.0)
141
142 def test_get_total_with_items(self):
143 """Test total calculation with items"""
144 self.cart.add_item("Apple", 1.50, 2) # 3.00
145 self.cart.add_item("Orange", 2.00, 1) # 2.00
146
147 self.assertEqual(self.cart.get_total(), 5.0)
148
149 def test_bulk_discount_small_order(self):
150 """Test bulk discount for small orders"""
151 # Add 10 items (less than 20)
152 for i in range(10):
153 self.cart.add_item(f"Item{i}", 1.0, 1)
154
155 self.cart.apply_bulk_discount()
156 self.assertEqual(self.cart.discount_rate, 0.0) # No discount
157
158 def test_bulk_discount_medium_order(self):
159 """Test bulk discount for medium orders"""
160 # Add 25 items (>= 20, < 50)
161 for i in range(25):
162 self.cart.add_item(f"Item{i}", 1.0, 1)
163
164 self.cart.apply_bulk_discount()
165 self.assertEqual(self.cart.discount_rate, 0.05) # 5% discount
166
167 def test_bulk_discount_large_order(self):
168 """Test bulk discount for large orders"""
169 # Add 60 items (>= 50, < 100)
170 for i in range(60):
171 self.cart.add_item(f"Item{i}", 1.0, 1)
172
173 self.cart.apply_bulk_discount()
174 self.assertEqual(self.cart.discount_rate, 0.10) # 10% discount
175
176 # Note: The >= 100 items case is not tested, so that branch won't be covered
177
178 def test_clear_cart(self):
179 """Test clearing cart"""
180 self.cart.add_item("Apple", 1.50, 2)
181 self.cart.apply_discount(0.1)
182
183 self.cart.clear()
184
185 self.assertTrue(self.cart.is_empty())
186 self.assertEqual(self.cart.discount_rate, 0.0)
187 self.assertEqual(self.cart.get_total(), 0.0)
188
189# Coverage reporting utility
190class CoverageReporter:
191 """Utility to analyze test coverage"""
192
193 def __init__(self, source_files):
194 self.source_files = source_files
195 self.covered_lines = set()
196 self.total_lines = set()
197
198 def analyze_coverage(self, test_results):
199 """Analyze coverage from test results"""
200 # This is a simplified example
201 # Real coverage tools use more sophisticated techniques
202
203 coverage_data = {
204 'lines_covered': len(self.covered_lines),
205 'lines_total': len(self.total_lines),
206 'coverage_percentage': (len(self.covered_lines) / len(self.total_lines)) * 100
207 }
208
209 return coverage_data
210
211 def generate_coverage_report(self):
212 """Generate coverage report"""
213 print("Coverage Report")
214 print("=" * 50)
215
216 for file in self.source_files:
217 print(f"File: {file}")
218 # Analyze each file's coverage
219 # This would involve parsing the source and tracking execution
220 print(f" Lines: 45/50 (90%)")
221 print(f" Missing: 12, 18, 25")Code Quality Metrics and Tools:
1# Example of code quality analysis
2
3import ast
4import inspect
5from typing import List, Dict, Any
6
7class CodeQualityAnalyzer:
8 """Analyze code quality metrics"""
9
10 def __init__(self):
11 self.metrics = {}
12
13 def analyze_function_complexity(self, func):
14 """Analyze cyclomatic complexity of a function"""
15 source = inspect.getsource(func)
16 tree = ast.parse(source)
17
18 complexity = 1 # Base complexity
19
20 for node in ast.walk(tree):
21 # Count decision points
22 if isinstance(node, (ast.If, ast.While, ast.For, ast.Try,
23 ast.With, ast.Assert, ast.Raise,
24 ast.BoolOp, ast.Compare)):
25 complexity += 1
26
27 return {
28 'cyclomatic_complexity': complexity,
29 'function_name': func.__name__,
30 'lines_of_code': len(source.split('
31')),
32 'complexity_rating': self._get_complexity_rating(complexity)
33 }
34
35 def _get_complexity_rating(self, complexity):
36 """Rate complexity level"""
37 if complexity <= 5:
38 return "Low (Good)"
39 elif complexity <= 10:
40 return "Moderate"
41 elif complexity <= 15:
42 return "High (Review needed)"
43 else:
44 return "Very High (Refactor recommended)"
45
46 def analyze_class_design(self, cls):
47 """Analyze class design metrics"""
48 methods = [method for method in dir(cls)
49 if callable(getattr(cls, method)) and not method.startswith('_')]
50
51 return {
52 'class_name': cls.__name__,
53 'public_methods': len(methods),
54 'total_attributes': len([attr for attr in dir(cls) if not attr.startswith('_')]),
55 'inheritance_depth': len(cls.__mro__) - 1,
56 'design_rating': self._get_design_rating(len(methods))
57 }
58
59 def _get_design_rating(self, method_count):
60 """Rate class design"""
61 if method_count <= 7:
62 return "Good (Focused responsibility)"
63 elif method_count <= 15:
64 return "Acceptable"
65 else:
66 return "Consider splitting (Too many responsibilities)"
67
68 def check_naming_conventions(self, obj):
69 """Check if naming follows Python conventions"""
70 name = obj.__name__
71 obj_type = type(obj).__name__
72
73 issues = []
74
75 if obj_type == 'function':
76 if not name.islower() or '_' not in name:
77 if len(name.split('_')) < 2 and len(name) > 3:
78 issues.append("Function names should use snake_case")
79
80 elif obj_type == 'type': # Class
81 if not name[0].isupper() or '_' in name:
82 issues.append("Class names should use PascalCase")
83
84 return {
85 'object_name': name,
86 'object_type': obj_type,
87 'naming_issues': issues,
88 'follows_convention': len(issues) == 0
89 }
90
91# Quality gates and continuous integration
92class QualityGate:
93 """Define quality gates for CI/CD"""
94
95 def __init__(self):
96 self.criteria = {
97 'test_coverage_minimum': 80.0,
98 'max_complexity_per_function': 10,
99 'max_methods_per_class': 15,
100 'max_line_length': 88,
101 'require_docstrings': True
102 }
103 self.results = []
104
105 def check_coverage_gate(self, coverage_percentage):
106 """Check if coverage meets minimum requirement"""
107 passed = coverage_percentage >= self.criteria['test_coverage_minimum']
108
109 result = {
110 'gate': 'test_coverage',
111 'passed': passed,
112 'actual': coverage_percentage,
113 'required': self.criteria['test_coverage_minimum'],
114 'message': f"Coverage: {coverage_percentage:.1f}% (required: {self.criteria['test_coverage_minimum']}%)"
115 }
116
117 self.results.append(result)
118 return passed
119
120 def check_complexity_gate(self, functions_complexity):
121 """Check if all functions meet complexity requirements"""
122 violations = []
123
124 for func_data in functions_complexity:
125 if func_data['cyclomatic_complexity'] > self.criteria['max_complexity_per_function']:
126 violations.append(func_data)
127
128 passed = len(violations) == 0
129
130 result = {
131 'gate': 'complexity',
132 'passed': passed,
133 'violations': violations,
134 'message': f"Complexity violations: {len(violations)} functions exceed limit of {self.criteria['max_complexity_per_function']}"
135 }
136
137 self.results.append(result)
138 return passed
139
140 def generate_quality_report(self):
141 """Generate comprehensive quality report"""
142 print("
143Code Quality Report")
144 print("=" * 50)
145
146 passed_gates = sum(1 for result in self.results if result['passed'])
147 total_gates = len(self.results)
148
149 print(f"Quality Gates: {passed_gates}/{total_gates} passed")
150 print()
151
152 for result in self.results:
153 status = "✓ PASS" if result['passed'] else "✗ FAIL"
154 print(f"{status} {result['gate'].upper()}: {result['message']}")
155
156 if 'violations' in result and result['violations']:
157 for violation in result['violations']:
158 print(f" - {violation['function_name']}: complexity {violation['cyclomatic_complexity']}")
159
160 overall_passed = passed_gates == total_gates
161 print(f"
162Overall Status: {'PASS' if overall_passed else 'FAIL'}")
163
164 return overall_passed
165
166# Example usage
167def example_quality_analysis():
168 """Demonstrate quality analysis"""
169 analyzer = CodeQualityAnalyzer()
170
171 # Analyze the ShoppingCart class
172 class_metrics = analyzer.analyze_class_design(ShoppingCart)
173 print(f"Class Analysis: {class_metrics}")
174
175 # Analyze a specific function
176 func_metrics = analyzer.analyze_function_complexity(ShoppingCart.apply_bulk_discount)
177 print(f"Function Analysis: {func_metrics}")
178
179 # Check naming conventions
180 naming_check = analyzer.check_naming_conventions(ShoppingCart)
181 print(f"Naming Check: {naming_check}")
182
183 # Quality gate example
184 gate = QualityGate()
185 gate.check_coverage_gate(85.5) # Pass
186 gate.check_complexity_gate([func_metrics]) # May pass or fail
187 gate.generate_quality_report()Code quality is multidimensional - good tests are necessary but not sufficient. Combine testing with static analysis, code reviews, and continuous monitoring for comprehensive quality assurance.
Build a comprehensive testing suite for a banking application demonstrating various testing patterns and debugging techniques
1import unittest
2from unittest.mock import Mock, patch, MagicMock
3from datetime import datetime, timedelta
4from typing import List, Dict, Optional, Any
5import logging
6import json
7import traceback
8from decimal import Decimal, InvalidOperation
9import sys
10
11# Configure logging for debugging
12logging.basicConfig(
13 level=logging.INFO,
14 format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
15)
16logger = logging.getLogger(__name__)
17
18class InsufficientFundsError(Exception):
19 """Custom exception for insufficient funds"""
20 pass
21
22class InvalidAccountError(Exception):
23 """Custom exception for invalid account operations"""
24 pass
25
26class Account:
27 """Bank account class with comprehensive functionality"""
28
29 def __init__(self, account_number: str, initial_balance: Decimal = Decimal('0')):
30 if not account_number or len(account_number) < 6:
31 raise InvalidAccountError("Account number must be at least 6 characters")
32
33 self.account_number = account_number
34 self._balance = Decimal(str(initial_balance))
35 self.transactions: List[Dict[str, Any]] = []
36 self.is_frozen = False
37 self.created_at = datetime.now()
38
39 logger.info(f"Account {account_number} created with balance {self._balance}")
40
41 @property
42 def balance(self) -> Decimal:
43 """Get current account balance"""
44 return self._balance
45
46 def deposit(self, amount: Decimal, description: str = "Deposit") -> bool:
47 """Deposit money into the account"""
48 try:
49 # Input validation
50 if not isinstance(amount, Decimal):
51 amount = Decimal(str(amount))
52
53 if amount <= 0:
54 raise ValueError("Deposit amount must be positive")
55
56 if self.is_frozen:
57 raise InvalidAccountError("Cannot deposit to frozen account")
58
59 # Perform deposit
60 old_balance = self._balance
61 self._balance += amount
62
63 # Record transaction
64 transaction = {
65 'type': 'deposit',
66 'amount': amount,
67 'balance_before': old_balance,
68 'balance_after': self._balance,
69 'description': description,
70 'timestamp': datetime.now().isoformat()
71 }
72 self.transactions.append(transaction)
73
74 logger.info(f"Deposited {amount} to {self.account_number}. New balance: {self._balance}")
75 return True
76
77 except (ValueError, InvalidOperation) as e:
78 logger.error(f"Deposit failed for {self.account_number}: {e}")
79 raise ValueError(f"Invalid deposit amount: {e}")
80
81 def withdraw(self, amount: Decimal, description: str = "Withdrawal") -> bool:
82 """Withdraw money from the account"""
83 try:
84 # Input validation
85 if not isinstance(amount, Decimal):
86 amount = Decimal(str(amount))
87
88 if amount <= 0:
89 raise ValueError("Withdrawal amount must be positive")
90
91 if self.is_frozen:
92 raise InvalidAccountError("Cannot withdraw from frozen account")
93
94 if amount > self._balance:
95 raise InsufficientFundsError(f"Insufficient funds. Balance: {self._balance}, Requested: {amount}")
96
97 # Perform withdrawal
98 old_balance = self._balance
99 self._balance -= amount
100
101 # Record transaction
102 transaction = {
103 'type': 'withdrawal',
104 'amount': amount,
105 'balance_before': old_balance,
106 'balance_after': self._balance,
107 'description': description,
108 'timestamp': datetime.now().isoformat()
109 }
110 self.transactions.append(transaction)
111
112 logger.info(f"Withdrew {amount} from {self.account_number}. New balance: {self._balance}")
113 return True
114
115 except (ValueError, InvalidOperation) as e:
116 logger.error(f"Withdrawal failed for {self.account_number}: {e}")
117 raise ValueError(f"Invalid withdrawal amount: {e}")
118
119 def transfer(self, target_account: 'Account', amount: Decimal, description: str = "Transfer") -> bool:
120 """Transfer money to another account"""
121 try:
122 # Validate target account
123 if not isinstance(target_account, Account):
124 raise ValueError("Target must be an Account instance")
125
126 if target_account.account_number == self.account_number:
127 raise ValueError("Cannot transfer to same account")
128
129 # Perform transfer (atomic operation)
130 transfer_description = f"{description} to {target_account.account_number}"
131 receive_description = f"{description} from {self.account_number}"
132
133 # Withdraw from source
134 self.withdraw(amount, transfer_description)
135
136 try:
137 # Deposit to target
138 target_account.deposit(amount, receive_description)
139 logger.info(f"Transfer completed: {amount} from {self.account_number} to {target_account.account_number}")
140 return True
141
142 except Exception as e:
143 # Rollback on failure
144 self.deposit(amount, f"Rollback: {transfer_description}")
145 raise e
146
147 except Exception as e:
148 logger.error(f"Transfer failed: {e}")
149 raise
150
151 def freeze(self, reason: str = "Administrative action"):
152 """Freeze the account"""
153 self.is_frozen = True
154 logger.warning(f"Account {self.account_number} frozen: {reason}")
155
156 def unfreeze(self):
157 """Unfreeze the account"""
158 self.is_frozen = False
159 logger.info(f"Account {self.account_number} unfrozen")
160
161 def get_transaction_history(self, limit: Optional[int] = None) -> List[Dict[str, Any]]:
162 """Get transaction history"""
163 transactions = self.transactions.copy()
164 if limit:
165 transactions = transactions[-limit:]
166 return transactions
167
168 def calculate_monthly_summary(self, month: int, year: int) -> Dict[str, Any]:
169 """Calculate monthly transaction summary"""
170 monthly_transactions = []
171 total_deposits = Decimal('0')
172 total_withdrawals = Decimal('0')
173
174 for transaction in self.transactions:
175 trans_date = datetime.fromisoformat(transaction['timestamp'])
176 if trans_date.month == month and trans_date.year == year:
177 monthly_transactions.append(transaction)
178 if transaction['type'] == 'deposit':
179 total_deposits += transaction['amount']
180 else:
181 total_withdrawals += transaction['amount']
182
183 return {
184 'month': month,
185 'year': year,
186 'transaction_count': len(monthly_transactions),
187 'total_deposits': total_deposits,
188 'total_withdrawals': total_withdrawals,
189 'net_change': total_deposits - total_withdrawals
190 }
191
192class BankingSystem:
193 """Banking system managing multiple accounts"""
194
195 def __init__(self):
196 self.accounts: Dict[str, Account] = {}
197 self.system_log: List[Dict[str, Any]] = []
198
199 def create_account(self, account_number: str, initial_balance: Decimal = Decimal('0')) -> Account:
200 """Create a new account"""
201 if account_number in self.accounts:
202 raise InvalidAccountError(f"Account {account_number} already exists")
203
204 account = Account(account_number, initial_balance)
205 self.accounts[account_number] = account
206
207 self.system_log.append({
208 'action': 'account_created',
209 'account_number': account_number,
210 'initial_balance': initial_balance,
211 'timestamp': datetime.now().isoformat()
212 })
213
214 return account
215
216 def get_account(self, account_number: str) -> Account:
217 """Get account by number"""
218 if account_number not in self.accounts:
219 raise InvalidAccountError(f"Account {account_number} not found")
220 return self.accounts[account_number]
221
222 def get_total_system_balance(self) -> Decimal:
223 """Calculate total balance across all accounts"""
224 return sum(account.balance for account in self.accounts.values())
225
226# Test Suite Implementation
227class TestAccount(unittest.TestCase):
228 """Comprehensive test suite for Account class"""
229
230 def setUp(self):
231 """Set up test fixtures before each test"""
232 self.account = Account("123456", Decimal('1000.00'))
233 self.empty_account = Account("789012", Decimal('0'))
234
235 def tearDown(self):
236 """Clean up after each test"""
237 # Reset any global state if needed
238 pass
239
240 def test_account_creation(self):
241 """Test account creation with valid inputs"""
242 account = Account("555666", Decimal('500.00'))
243 self.assertEqual(account.account_number, "555666")
244 self.assertEqual(account.balance, Decimal('500.00'))
245 self.assertFalse(account.is_frozen)
246 self.assertEqual(len(account.transactions), 0)
247
248 def test_account_creation_invalid_number(self):
249 """Test account creation with invalid account number"""
250 with self.assertRaises(InvalidAccountError):
251 Account("123") # Too short
252
253 with self.assertRaises(InvalidAccountError):
254 Account("") # Empty
255
256 def test_deposit_valid_amount(self):
257 """Test depositing valid amounts"""
258 initial_balance = self.account.balance
259 result = self.account.deposit(Decimal('250.50'))
260
261 self.assertTrue(result)
262 self.assertEqual(self.account.balance, initial_balance + Decimal('250.50'))
263 self.assertEqual(len(self.account.transactions), 1)
264 self.assertEqual(self.account.transactions[0]['type'], 'deposit')
265
266 def test_deposit_invalid_amounts(self):
267 """Test depositing invalid amounts"""
268 # Negative amount
269 with self.assertRaises(ValueError):
270 self.account.deposit(Decimal('-100'))
271
272 # Zero amount
273 with self.assertRaises(ValueError):
274 self.account.deposit(Decimal('0'))
275
276 # Invalid string that can't be converted
277 with self.assertRaises(ValueError):
278 self.account.deposit("invalid")
279
280 def test_deposit_to_frozen_account(self):
281 """Test depositing to frozen account"""
282 self.account.freeze()
283 with self.assertRaises(InvalidAccountError):
284 self.account.deposit(Decimal('100'))
285
286 def test_withdrawal_valid_amount(self):
287 """Test withdrawing valid amounts"""
288 initial_balance = self.account.balance
289 result = self.account.withdraw(Decimal('200.00'))
290
291 self.assertTrue(result)
292 self.assertEqual(self.account.balance, initial_balance - Decimal('200.00'))
293 self.assertEqual(len(self.account.transactions), 1)
294 self.assertEqual(self.account.transactions[0]['type'], 'withdrawal')
295
296 def test_withdrawal_insufficient_funds(self):
297 """Test withdrawing more than available balance"""
298 with self.assertRaises(InsufficientFundsError):
299 self.account.withdraw(Decimal('2000.00'))
300
301 def test_withdrawal_from_frozen_account(self):
302 """Test withdrawing from frozen account"""
303 self.account.freeze()
304 with self.assertRaises(InvalidAccountError):
305 self.account.withdraw(Decimal('100'))
306
307 def test_transfer_success(self):
308 """Test successful transfer between accounts"""
309 target_account = Account("999888", Decimal('500'))
310 transfer_amount = Decimal('300')
311
312 initial_source_balance = self.account.balance
313 initial_target_balance = target_account.balance
314
315 result = self.account.transfer(target_account, transfer_amount)
316
317 self.assertTrue(result)
318 self.assertEqual(self.account.balance, initial_source_balance - transfer_amount)
319 self.assertEqual(target_account.balance, initial_target_balance + transfer_amount)
320
321 def test_transfer_insufficient_funds(self):
322 """Test transfer with insufficient funds"""
323 target_account = Account("999888", Decimal('500'))
324
325 with self.assertRaises(InsufficientFundsError):
326 self.account.transfer(target_account, Decimal('2000'))
327
328 def test_transfer_to_same_account(self):
329 """Test transfer to same account (should fail)"""
330 with self.assertRaises(ValueError):
331 self.account.transfer(self.account, Decimal('100'))
332
333 def test_freeze_and_unfreeze(self):
334 """Test account freezing and unfreezing"""
335 self.assertFalse(self.account.is_frozen)
336
337 self.account.freeze("Testing")
338 self.assertTrue(self.account.is_frozen)
339
340 self.account.unfreeze()
341 self.assertFalse(self.account.is_frozen)
342
343 def test_transaction_history(self):
344 """Test transaction history recording"""
345 self.account.deposit(Decimal('100'), "Test deposit")
346 self.account.withdraw(Decimal('50'), "Test withdrawal")
347
348 history = self.account.get_transaction_history()
349 self.assertEqual(len(history), 2)
350
351 # Test limited history
352 limited_history = self.account.get_transaction_history(limit=1)
353 self.assertEqual(len(limited_history), 1)
354 self.assertEqual(limited_history[0]['type'], 'withdrawal') # Should be the most recent
355
356 def test_monthly_summary(self):
357 """Test monthly transaction summary"""
358 current_date = datetime.now()
359 self.account.deposit(Decimal('500'), "Monthly deposit 1")
360 self.account.deposit(Decimal('300'), "Monthly deposit 2")
361 self.account.withdraw(Decimal('200'), "Monthly withdrawal")
362
363 summary = self.account.calculate_monthly_summary(current_date.month, current_date.year)
364
365 self.assertEqual(summary['transaction_count'], 3)
366 self.assertEqual(summary['total_deposits'], Decimal('800'))
367 self.assertEqual(summary['total_withdrawals'], Decimal('200'))
368 self.assertEqual(summary['net_change'], Decimal('600'))
369
370class TestBankingSystem(unittest.TestCase):
371 """Test suite for BankingSystem class"""
372
373 def setUp(self):
374 """Set up banking system for tests"""
375 self.banking_system = BankingSystem()
376
377 def test_create_account(self):
378 """Test account creation in banking system"""
379 account = self.banking_system.create_account("111222", Decimal('1000'))
380
381 self.assertIn("111222", self.banking_system.accounts)
382 self.assertEqual(account.balance, Decimal('1000'))
383
384 def test_create_duplicate_account(self):
385 """Test creating account with existing number"""
386 self.banking_system.create_account("111222", Decimal('1000'))
387
388 with self.assertRaises(InvalidAccountError):
389 self.banking_system.create_account("111222", Decimal('500'))
390
391 def test_get_existing_account(self):
392 """Test retrieving existing account"""
393 original_account = self.banking_system.create_account("333444", Decimal('750'))
394 retrieved_account = self.banking_system.get_account("333444")
395
396 self.assertEqual(original_account, retrieved_account)
397
398 def test_get_nonexistent_account(self):
399 """Test retrieving non-existent account"""
400 with self.assertRaises(InvalidAccountError):
401 self.banking_system.get_account("999999")
402
403 def test_total_system_balance(self):
404 """Test total system balance calculation"""
405 self.banking_system.create_account("111111", Decimal('1000'))
406 self.banking_system.create_account("222222", Decimal('1500'))
407 self.banking_system.create_account("333333", Decimal('750'))
408
409 total = self.banking_system.get_total_system_balance()
410 self.assertEqual(total, Decimal('3250'))
411
412class TestIntegration(unittest.TestCase):
413 """Integration tests for complete banking operations"""
414
415 def setUp(self):
416 """Set up integration test environment"""
417 self.banking_system = BankingSystem()
418 self.account1 = self.banking_system.create_account("ACC001", Decimal('2000'))
419 self.account2 = self.banking_system.create_account("ACC002", Decimal('1500'))
420
421 def test_complex_transaction_scenario(self):
422 """Test complex transaction scenario"""
423 # Initial state check
424 self.assertEqual(self.account1.balance, Decimal('2000'))
425 self.assertEqual(self.account2.balance, Decimal('1500'))
426
427 # Perform various operations
428 self.account1.deposit(Decimal('500')) # Account1: 2500
429 self.account2.withdraw(Decimal('300')) # Account2: 1200
430 self.account1.transfer(self.account2, Decimal('800')) # Account1: 1700, Account2: 2000
431
432 # Verify final balances
433 self.assertEqual(self.account1.balance, Decimal('1700'))
434 self.assertEqual(self.account2.balance, Decimal('2000'))
435
436 # Verify total system balance unchanged (except for the net transactions)
437 expected_total = Decimal('2000') + Decimal('1500') + Decimal('500') - Decimal('300')
438 self.assertEqual(self.banking_system.get_total_system_balance(), expected_total)
439
440class MockTestExample(unittest.TestCase):
441 """Example of using mocks in testing"""
442
443 @patch('datetime.datetime')
444 def test_transaction_timestamp_mocking(self, mock_datetime):
445 """Test with mocked datetime"""
446 # Set up mock
447 mock_datetime.now.return_value = datetime(2023, 6, 15, 14, 30, 0)
448 mock_datetime.fromisoformat = datetime.fromisoformat
449
450 account = Account("MOCK123", Decimal('1000'))
451 account.deposit(Decimal('100'))
452
453 # Verify transaction has mocked timestamp
454 transaction = account.transactions[0]
455 self.assertEqual(transaction['timestamp'], '2023-06-15T14:30:00')
456
457 def test_transfer_with_mock_target(self):
458 """Test transfer with mocked target account"""
459 source_account = Account("SOURCE", Decimal('1000'))
460
461 # Create mock target account
462 mock_target = Mock(spec=Account)
463 mock_target.account_number = "TARGET"
464 mock_target.deposit.return_value = True
465
466 # This would normally raise an exception due to type checking,
467 # but demonstrates the concept
468 try:
469 source_account.transfer(mock_target, Decimal('100'))
470 except ValueError as e:
471 # Expected due to isinstance check
472 self.assertIn("Target must be an Account instance", str(e))
473
474def create_test_suite():
475 """Create a comprehensive test suite"""
476 suite = unittest.TestSuite()
477
478 # Add test cases
479 suite.addTest(unittest.makeSuite(TestAccount))
480 suite.addTest(unittest.makeSuite(TestBankingSystem))
481 suite.addTest(unittest.makeSuite(TestIntegration))
482 suite.addTest(unittest.makeSuite(MockTestExample))
483
484 return suite
485
486def debug_example():
487 """Example demonstrating debugging techniques"""
488 print("\n=== Debugging Example ===")
489
490 try:
491 # Intentionally create an error scenario
492 account = Account("DEBUG123", Decimal('500'))
493
494 # This will succeed
495 account.deposit(Decimal('100'))
496 print(f"Balance after deposit: {account.balance}")
497
498 # This will fail - demonstrating exception handling
499 account.withdraw(Decimal('1000')) # More than available
500
501 except InsufficientFundsError as e:
502 print(f"Caught expected error: {e}")
503
504 # Demonstrate logging for debugging
505 logger.error(f"Debugging info: Attempted withdrawal of 1000 from account with balance {account.balance}")
506
507 # Demonstrate traceback for debugging
508 print("\nFull traceback:")
509 traceback.print_exc()
510
511 except Exception as e:
512 print(f"Unexpected error: {e}")
513 logger.critical(f"Critical error in debug example: {e}")
514
515def performance_test():
516 """Example performance test"""
517 print("\n=== Performance Test Example ===")
518
519 import time
520
521 # Create account and measure performance
522 start_time = time.time()
523
524 account = Account("PERF123", Decimal('10000'))
525
526 # Perform many operations
527 for i in range(1000):
528 if i % 2 == 0:
529 account.deposit(Decimal('10'))
530 else:
531 account.withdraw(Decimal('5'))
532
533 end_time = time.time()
534 duration = end_time - start_time
535
536 print(f"Performed 1000 transactions in {duration:.4f} seconds")
537 print(f"Final balance: {account.balance}")
538 print(f"Transaction count: {len(account.transactions)}")
539
540# Example usage demonstrating comprehensive testing
541def main():
542 """Main function demonstrating the testing system"""
543 print("=== Banking System Testing and Debugging Demo ===")
544
545 # Run debugging example
546 debug_example()
547
548 # Run performance test
549 performance_test()
550
551 # Create and run test suite
552 print("\n=== Running Comprehensive Test Suite ===")
553
554 # Create test runner with verbosity
555 runner = unittest.TextTestRunner(verbosity=2)
556
557 # Create test suite
558 suite = create_test_suite()
559
560 # Run tests
561 result = runner.run(suite)
562
563 # Print summary
564 print(f"\n=== Test Results Summary ===")
565 print(f"Tests run: {result.testsRun}")
566 print(f"Failures: {len(result.failures)}")
567 print(f"Errors: {len(result.errors)}")
568
569 if result.failures:
570 print("\nFailures:")
571 for test, traceback in result.failures:
572 print(f"- {test}: {traceback}")
573
574 if result.errors:
575 print("\nErrors:")
576 for test, traceback in result.errors:
577 print(f"- {test}: {traceback}")
578
579 print("\n=== Testing and Debugging Features Demonstrated ===")
580 print("- Unit test creation with setUp() and tearDown()")
581 print("- Comprehensive assertion methods and error testing")
582 print("- Integration testing across multiple components")
583 print("- Mock object usage for isolated testing")
584 print("- Exception handling and custom exception testing")
585 print("- Test suite organization and test discovery")
586 print("- Debugging with logging and traceback analysis")
587 print("- Performance testing and measurement")
588 print("- Test-driven development patterns")
589
590 print("\n=== Key Testing Best Practices ===")
591 print("- Write tests before or alongside code development")
592 print("- Test both success and failure scenarios")
593 print("- Use descriptive test names that explain what's being tested")
594 print("- Keep tests isolated and independent")
595 print("- Use appropriate assertion methods for clear error messages")
596 print("- Mock external dependencies for unit test isolation")
597 print("- Include edge cases and boundary conditions")
598 print("- Measure test coverage but focus on quality over quantity")
599
600if __name__ == "__main__":
601 main()Your progress is saved in this browser, on this device only.