1"""
2Python Testing
3
4Demonstrates:
5- unittest basics
6- Test fixtures and setup/teardown
7- Assertions
8- pytest-style testing
9- Mocking and patching
10- Parametrized tests
11- Test organization
12"""
13
14import unittest
15from unittest.mock import Mock, patch, MagicMock
16from typing import List
17
18
19def section(title: str) -> None:
20 """Print a section header."""
21 print("\n" + "=" * 60)
22 print(f" {title}")
23 print("=" * 60)
24
25
26# =============================================================================
27# Code to Test
28# =============================================================================
29
30class Calculator:
31 """Simple calculator for testing."""
32
33 def add(self, a: float, b: float) -> float:
34 return a + b
35
36 def divide(self, a: float, b: float) -> float:
37 if b == 0:
38 raise ValueError("Cannot divide by zero")
39 return a / b
40
41 def is_even(self, n: int) -> bool:
42 return n % 2 == 0
43
44
45class BankAccount:
46 """Bank account for testing."""
47
48 def __init__(self, balance: float = 0):
49 self._balance = balance
50
51 @property
52 def balance(self) -> float:
53 return self._balance
54
55 def deposit(self, amount: float):
56 if amount <= 0:
57 raise ValueError("Deposit amount must be positive")
58 self._balance += amount
59
60 def withdraw(self, amount: float):
61 if amount <= 0:
62 raise ValueError("Withdrawal amount must be positive")
63 if amount > self._balance:
64 raise ValueError("Insufficient funds")
65 self._balance -= amount
66
67
68class UserService:
69 """User service that depends on database."""
70
71 def __init__(self, database):
72 self.database = database
73
74 def get_user(self, user_id: int):
75 return self.database.get(user_id)
76
77 def create_user(self, name: str, email: str):
78 user = {"name": name, "email": email}
79 return self.database.save(user)
80
81
82# =============================================================================
83# unittest Basic Tests
84# =============================================================================
85
86section("unittest Basic Tests")
87
88
89class TestCalculator(unittest.TestCase):
90 """Test cases for Calculator."""
91
92 def setUp(self):
93 """Set up test fixtures - runs before each test."""
94 self.calc = Calculator()
95
96 def tearDown(self):
97 """Clean up after each test."""
98 self.calc = None
99
100 def test_add(self):
101 """Test addition."""
102 result = self.calc.add(2, 3)
103 self.assertEqual(result, 5)
104
105 def test_add_negative(self):
106 """Test addition with negative numbers."""
107 result = self.calc.add(-1, -2)
108 self.assertEqual(result, -3)
109
110 def test_divide(self):
111 """Test division."""
112 result = self.calc.divide(10, 2)
113 self.assertEqual(result, 5)
114
115 def test_divide_by_zero(self):
116 """Test that dividing by zero raises error."""
117 with self.assertRaises(ValueError):
118 self.calc.divide(10, 0)
119
120 def test_is_even(self):
121 """Test even number detection."""
122 self.assertTrue(self.calc.is_even(4))
123 self.assertFalse(self.calc.is_even(5))
124
125
126# =============================================================================
127# Assertion Methods
128# =============================================================================
129
130section("Common Assertion Methods")
131
132
133class TestAssertions(unittest.TestCase):
134 """Demonstrate various assertion methods."""
135
136 def test_equality(self):
137 """Test equality assertions."""
138 self.assertEqual(1 + 1, 2)
139 self.assertNotEqual(1 + 1, 3)
140
141 def test_boolean(self):
142 """Test boolean assertions."""
143 self.assertTrue(True)
144 self.assertFalse(False)
145
146 def test_none(self):
147 """Test None assertions."""
148 self.assertIsNone(None)
149 self.assertIsNotNone(42)
150
151 def test_in(self):
152 """Test membership assertions."""
153 self.assertIn(1, [1, 2, 3])
154 self.assertNotIn(4, [1, 2, 3])
155
156 def test_instance(self):
157 """Test type assertions."""
158 self.assertIsInstance("hello", str)
159 self.assertNotIsInstance("hello", int)
160
161 def test_almost_equal(self):
162 """Test floating point equality."""
163 self.assertAlmostEqual(0.1 + 0.2, 0.3, places=7)
164
165 def test_greater_less(self):
166 """Test comparison assertions."""
167 self.assertGreater(10, 5)
168 self.assertLess(5, 10)
169 self.assertGreaterEqual(10, 10)
170 self.assertLessEqual(5, 5)
171
172
173# =============================================================================
174# Test Fixtures
175# =============================================================================
176
177section("Test Fixtures and Setup")
178
179
180class TestBankAccount(unittest.TestCase):
181 """Test bank account with fixtures."""
182
183 @classmethod
184 def setUpClass(cls):
185 """Run once before all tests in class."""
186 print(" setUpClass: Initialize shared resources")
187 cls.bank_name = "Test Bank"
188
189 @classmethod
190 def tearDownClass(cls):
191 """Run once after all tests in class."""
192 print(" tearDownClass: Cleanup shared resources")
193
194 def setUp(self):
195 """Run before each test."""
196 self.account = BankAccount(100)
197
198 def test_initial_balance(self):
199 """Test initial balance."""
200 self.assertEqual(self.account.balance, 100)
201
202 def test_deposit(self):
203 """Test deposit."""
204 self.account.deposit(50)
205 self.assertEqual(self.account.balance, 150)
206
207 def test_deposit_negative(self):
208 """Test that negative deposit raises error."""
209 with self.assertRaises(ValueError):
210 self.account.deposit(-10)
211
212 def test_withdraw(self):
213 """Test withdrawal."""
214 self.account.withdraw(30)
215 self.assertEqual(self.account.balance, 70)
216
217 def test_withdraw_insufficient_funds(self):
218 """Test withdrawal with insufficient funds."""
219 with self.assertRaises(ValueError):
220 self.account.withdraw(200)
221
222
223# =============================================================================
224# Mocking and Patching
225# =============================================================================
226
227section("Mocking and Patching")
228
229
230class TestUserServiceWithMock(unittest.TestCase):
231 """Test UserService using mocks."""
232
233 def test_get_user_with_mock(self):
234 """Test get_user with mocked database."""
235 # Create mock database
236 mock_db = Mock()
237 mock_db.get.return_value = {"id": 1, "name": "Alice"}
238
239 service = UserService(mock_db)
240 user = service.get_user(1)
241
242 # Verify mock was called correctly
243 mock_db.get.assert_called_once_with(1)
244 self.assertEqual(user["name"], "Alice")
245
246 def test_create_user_with_mock(self):
247 """Test create_user with mocked database."""
248 mock_db = Mock()
249 mock_db.save.return_value = {"id": 2, "name": "Bob", "email": "bob@example.com"}
250
251 service = UserService(mock_db)
252 user = service.create_user("Bob", "bob@example.com")
253
254 # Verify save was called
255 self.assertTrue(mock_db.save.called)
256 call_args = mock_db.save.call_args[0][0]
257 self.assertEqual(call_args["name"], "Bob")
258
259
260class TestWithPatch(unittest.TestCase):
261 """Test using patch decorator."""
262
263 @patch('__main__.BankAccount')
264 def test_with_patch_decorator(self, mock_account_class):
265 """Test using patch as decorator."""
266 # Configure mock
267 mock_instance = mock_account_class.return_value
268 mock_instance.balance = 1000
269
270 # Create instance (will be mock)
271 account = BankAccount()
272
273 self.assertEqual(account.balance, 1000)
274
275 def test_with_patch_context_manager(self):
276 """Test using patch as context manager."""
277 with patch('__main__.Calculator') as mock_calc_class:
278 mock_instance = mock_calc_class.return_value
279 mock_instance.add.return_value = 42
280
281 calc = Calculator()
282 result = calc.add(1, 2)
283
284 self.assertEqual(result, 42)
285 mock_instance.add.assert_called_once_with(1, 2)
286
287
288# =============================================================================
289# Mock Object Behavior
290# =============================================================================
291
292section("Mock Object Behavior")
293
294
295class TestMockBehavior(unittest.TestCase):
296 """Demonstrate mock object features."""
297
298 def test_mock_return_value(self):
299 """Test setting mock return value."""
300 mock = Mock()
301 mock.method.return_value = 42
302
303 result = mock.method()
304 self.assertEqual(result, 42)
305
306 def test_mock_side_effect(self):
307 """Test mock side effect for multiple calls."""
308 mock = Mock()
309 mock.method.side_effect = [1, 2, 3]
310
311 self.assertEqual(mock.method(), 1)
312 self.assertEqual(mock.method(), 2)
313 self.assertEqual(mock.method(), 3)
314
315 def test_mock_side_effect_exception(self):
316 """Test mock raising exception."""
317 mock = Mock()
318 mock.method.side_effect = ValueError("Test error")
319
320 with self.assertRaises(ValueError):
321 mock.method()
322
323 def test_mock_call_count(self):
324 """Test tracking call count."""
325 mock = Mock()
326
327 mock.method()
328 mock.method()
329 mock.method()
330
331 self.assertEqual(mock.method.call_count, 3)
332
333 def test_magic_mock(self):
334 """Test MagicMock with magic methods."""
335 mock = MagicMock()
336
337 # MagicMock supports magic methods
338 mock.__len__.return_value = 5
339 self.assertEqual(len(mock), 5)
340
341 mock.__getitem__.return_value = 42
342 self.assertEqual(mock[0], 42)
343
344
345# =============================================================================
346# Subtest Pattern
347# =============================================================================
348
349section("Subtests")
350
351
352class TestWithSubtests(unittest.TestCase):
353 """Demonstrate subtests for multiple similar tests."""
354
355 def test_is_even_with_subtests(self):
356 """Test multiple values using subtests."""
357 calc = Calculator()
358
359 test_cases = [
360 (2, True),
361 (4, True),
362 (6, True),
363 (1, False),
364 (3, False),
365 (5, False),
366 ]
367
368 for number, expected in test_cases:
369 with self.subTest(number=number):
370 result = calc.is_even(number)
371 self.assertEqual(result, expected)
372
373
374# =============================================================================
375# Pytest-Style Tests (Without pytest)
376# =============================================================================
377
378section("Pytest-Style Tests")
379
380
381def test_calculator_add():
382 """Simple test function (pytest-style)."""
383 calc = Calculator()
384 assert calc.add(2, 3) == 5
385 assert calc.add(-1, 1) == 0
386
387
388def test_calculator_divide():
389 """Test with exception (pytest-style)."""
390 calc = Calculator()
391 assert calc.divide(10, 2) == 5
392
393 try:
394 calc.divide(10, 0)
395 assert False, "Should have raised ValueError"
396 except ValueError:
397 pass # Expected
398
399
400# =============================================================================
401# Parametrized Test Pattern
402# =============================================================================
403
404section("Parametrized Test Pattern")
405
406
407class TestParametrized(unittest.TestCase):
408 """Parametrized test pattern."""
409
410 def test_add_parametrized(self):
411 """Test add with multiple parameter sets."""
412 calc = Calculator()
413
414 test_cases = [
415 (2, 3, 5),
416 (0, 0, 0),
417 (-1, 1, 0),
418 (10, -5, 5),
419 (1.5, 2.5, 4.0),
420 ]
421
422 for a, b, expected in test_cases:
423 with self.subTest(a=a, b=b):
424 result = calc.add(a, b)
425 self.assertAlmostEqual(result, expected)
426
427
428# =============================================================================
429# Running Tests
430# =============================================================================
431
432section("Running Tests")
433
434print("""
435Run tests with:
436 python -m unittest test_module.py
437 python -m unittest test_module.TestClass
438 python -m unittest test_module.TestClass.test_method
439 python -m unittest discover
440
441Verbose output:
442 python -m unittest -v test_module.py
443
444With pytest (if installed):
445 pytest test_module.py
446 pytest -v test_module.py
447 pytest -k "test_add" # Run tests matching pattern
448""")
449
450
451# Run a subset of tests for demonstration
452if __name__ == "__main__":
453 # Create test suite
454 loader = unittest.TestLoader()
455 suite = unittest.TestSuite()
456
457 # Add specific test classes
458 suite.addTests(loader.loadTestsFromTestCase(TestCalculator))
459 suite.addTests(loader.loadTestsFromTestCase(TestBankAccount))
460 suite.addTests(loader.loadTestsFromTestCase(TestUserServiceWithMock))
461
462 # Run tests
463 runner = unittest.TextTestRunner(verbosity=2)
464 print("\n" + "=" * 60)
465 print(" Running Test Suite")
466 print("=" * 60)
467 result = runner.run(suite)
468
469 # Summary
470 section("Test Summary")
471 print(f"Tests run: {result.testsRun}")
472 print(f"Successes: {result.testsRun - len(result.failures) - len(result.errors)}")
473 print(f"Failures: {len(result.failures)}")
474 print(f"Errors: {len(result.errors)}")