08_testing.py

Download
python 475 lines 13.1 KB
  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)}")