python

examples

examples.pyšŸ
"""
Testing in Python - Practical Examples
This file demonstrates unittest, pytest patterns, and mocking
"""

# ============================================================
# 1. UNITTEST BASICS
# ============================================================

print("=" * 60)
print("1. UNITTEST BASICS")
print("=" * 60)

import unittest
from io import StringIO
import sys

# Sample code to test
class Calculator:
    """Simple calculator class for demonstration."""
    
    def add(self, a, b):
        return a + b
    
    def subtract(self, a, b):
        return a - b
    
    def multiply(self, a, b):
        return a * b
    
    def divide(self, a, b):
        if b == 0:
            raise ValueError("Cannot divide by zero")
        return a / b


class TestCalculator(unittest.TestCase):
    """Test cases for Calculator class."""
    
    def setUp(self):
        """Create a calculator instance before each test."""
        self.calc = Calculator()
    
    def test_add(self):
        """Test addition."""
        self.assertEqual(self.calc.add(2, 3), 5)
        self.assertEqual(self.calc.add(-1, 1), 0)
        self.assertEqual(self.calc.add(0, 0), 0)
    
    def test_subtract(self):
        """Test subtraction."""
        self.assertEqual(self.calc.subtract(5, 3), 2)
        self.assertEqual(self.calc.subtract(0, 5), -5)
    
    def test_multiply(self):
        """Test multiplication."""
        self.assertEqual(self.calc.multiply(3, 4), 12)
        self.assertEqual(self.calc.multiply(-2, 3), -6)
        self.assertEqual(self.calc.multiply(0, 100), 0)
    
    def test_divide(self):
        """Test division."""
        self.assertEqual(self.calc.divide(10, 2), 5)
        self.assertAlmostEqual(self.calc.divide(1, 3), 0.333333, places=5)
    
    def test_divide_by_zero(self):
        """Test that dividing by zero raises ValueError."""
        with self.assertRaises(ValueError) as context:
            self.calc.divide(10, 0)
        self.assertIn("zero", str(context.exception))


# Run unittest programmatically
def run_unittest_demo():
    """Run the unittest suite and capture output."""
    suite = unittest.TestLoader().loadTestsFromTestCase(TestCalculator)
    runner = unittest.TextTestRunner(verbosity=2, stream=StringIO())
    result = runner.run(suite)
    
    print(f"\nTests run: {result.testsRun}")
    print(f"Failures: {len(result.failures)}")
    print(f"Errors: {len(result.errors)}")
    print(f"Success: {result.wasSuccessful()}")


run_unittest_demo()


# ============================================================
# 2. UNITTEST ASSERTIONS
# ============================================================

print("\n" + "=" * 60)
print("2. UNITTEST ASSERTIONS")
print("=" * 60)

class TestAssertions(unittest.TestCase):
    """Demonstrate various assertion methods."""
    
    def test_equality(self):
        """Equality assertions."""
        self.assertEqual(1 + 1, 2)
        self.assertNotEqual(1 + 1, 3)
    
    def test_truthiness(self):
        """Boolean assertions."""
        self.assertTrue(1 < 2)
        self.assertFalse(1 > 2)
    
    def test_identity(self):
        """Identity assertions."""
        a = [1, 2, 3]
        b = a
        c = [1, 2, 3]
        
        self.assertIs(a, b)       # Same object
        self.assertIsNot(a, c)    # Different objects
        self.assertEqual(a, c)     # But equal values
    
    def test_none(self):
        """None assertions."""
        self.assertIsNone(None)
        self.assertIsNotNone("something")
    
    def test_membership(self):
        """Membership assertions."""
        self.assertIn(2, [1, 2, 3])
        self.assertNotIn(4, [1, 2, 3])
    
    def test_types(self):
        """Type assertions."""
        self.assertIsInstance("hello", str)
        self.assertIsInstance(42, int)
        self.assertIsInstance([1, 2], list)
    
    def test_comparisons(self):
        """Comparison assertions."""
        self.assertGreater(5, 3)
        self.assertGreaterEqual(5, 5)
        self.assertLess(3, 5)
        self.assertLessEqual(5, 5)
    
    def test_almost_equal(self):
        """Floating point assertions."""
        self.assertAlmostEqual(0.1 + 0.2, 0.3, places=10)


# Run assertions demo
suite = unittest.TestLoader().loadTestsFromTestCase(TestAssertions)
runner = unittest.TextTestRunner(verbosity=0, stream=StringIO())
result = runner.run(suite)
print(f"Assertion tests passed: {result.testsRun - len(result.failures)}/{result.testsRun}")


# ============================================================
# 3. SETUP AND TEARDOWN
# ============================================================

print("\n" + "=" * 60)
print("3. SETUP AND TEARDOWN")
print("=" * 60)

class TestWithSetupTeardown(unittest.TestCase):
    """Demonstrate setup and teardown methods."""
    
    @classmethod
    def setUpClass(cls):
        """Run once before all tests in this class."""
        print("  [setUpClass] Running once before all tests")
        cls.shared_resource = "Shared data"
    
    @classmethod
    def tearDownClass(cls):
        """Run once after all tests in this class."""
        print("  [tearDownClass] Cleaning up after all tests")
        cls.shared_resource = None
    
    def setUp(self):
        """Run before each test."""
        print(f"    [setUp] Preparing for {self._testMethodName}")
        self.test_data: list[int] = [1, 2, 3]
    
    def tearDown(self):
        """Run after each test."""
        print(f"    [tearDown] Cleaning up after {self._testMethodName}")
        self.test_data = []
    
    def test_first(self):
        """First test."""
        print("      Running test_first")
        self.test_data.append(4)
        self.assertEqual(len(self.test_data), 4)
    
    def test_second(self):
        """Second test - has fresh data."""
        print("      Running test_second")
        # test_data is [1, 2, 3], not [1, 2, 3, 4]
        self.assertEqual(len(self.test_data), 3)


# Run setup/teardown demo
print("\nSetup/Teardown execution order:")
suite = unittest.TestLoader().loadTestsFromTestCase(TestWithSetupTeardown)
runner = unittest.TextTestRunner(verbosity=0, stream=StringIO())
runner.run(suite)


# ============================================================
# 4. PYTEST-STYLE TESTS (can run with pytest)
# ============================================================

print("\n" + "=" * 60)
print("4. PYTEST-STYLE TESTS")
print("=" * 60)

# Simple pytest-style test functions
def test_simple_assertion():
    """Pytest uses simple assert statements."""
    assert 1 + 1 == 2

def test_string_operations():
    """Test string methods."""
    s = "hello world"
    assert s.upper() == "HELLO WORLD"
    assert s.split() == ["hello", "world"]
    assert s.replace("world", "python") == "hello python"

def test_list_operations():
    """Test list operations."""
    items = [1, 2, 3]
    items.append(4)
    assert len(items) == 4
    assert 4 in items
    assert items[-1] == 4

def test_exception_handling():
    """Test exception is raised."""
    try:
        int("not a number")
        assert False, "Should have raised ValueError"
    except ValueError:
        pass  # Expected

# Note: These would normally be in a separate test file and run with pytest


# ============================================================
# 5. MOCKING BASICS
# ============================================================

print("\n" + "=" * 60)
print("5. MOCKING BASICS")
print("=" * 60)

from unittest.mock import Mock, MagicMock, patch

# Basic Mock usage
print("--- Basic Mock ---")
mock = Mock()

# Mock methods return new mocks by default
result = mock.some_method()
print(f"Calling undefined method returns: {type(result).__name__}")

# Set return values
mock.get_value.return_value = 42
print(f"get_value() returns: {mock.get_value()}")

# Track calls
mock.process("data1")
mock.process("data2")
mock.process("data3")
print(f"process() called {mock.process.call_count} times")
print(f"Call arguments: {mock.process.call_args_list}")

# Assertions
mock.process.assert_called()  # Was called at least once
mock.process.assert_called_with("data3")  # Last call was with "data3"


print("\n--- MagicMock ---")
magic = MagicMock()

# MagicMock supports magic methods
magic.__len__.return_value = 5
print(f"len(magic) = {len(magic)}")

magic.__getitem__.return_value = "item"
print(f"magic[0] = {magic[0]}")
print(f"magic['key'] = {magic['key']}")

magic.__iter__.return_value = iter([1, 2, 3])
print(f"list(magic) = {list(magic)}")


# ============================================================
# 6. PATCHING
# ============================================================

print("\n" + "=" * 60)
print("6. PATCHING")
print("=" * 60)

# Code to test
import random

def roll_dice():
    """Roll a 6-sided die."""
    return random.randint(1, 6)

def is_lucky_roll():
    """Return True if we roll a 6."""
    return roll_dice() == 6


# Test without mocking - unpredictable
print("--- Without Mocking (unpredictable) ---")
results = [is_lucky_roll() for _ in range(10)]
print(f"Results vary: {results}")


# Test with patching - controlled
print("\n--- With Patching (controlled) ---")

# Patch as decorator
@patch('random.randint')
def test_lucky_roll_true(mock_randint):
    mock_randint.return_value = 6
    result = is_lucky_roll()
    print(f"When dice returns 6, is_lucky = {result}")
    return result

@patch('random.randint')  
def test_lucky_roll_false(mock_randint):
    mock_randint.return_value = 3
    result = is_lucky_roll()
    print(f"When dice returns 3, is_lucky = {result}")
    return result

test_lucky_roll_true()
test_lucky_roll_false()


# Patch as context manager
print("\n--- Patching with Context Manager ---")
with patch('random.randint') as mock_randint:
    mock_randint.return_value = 6
    print(f"In context: is_lucky = {is_lucky_roll()}")

print(f"Outside context: is_lucky = {is_lucky_roll()} (real random)")


# ============================================================
# 7. MOCKING EXTERNAL SERVICES
# ============================================================

print("\n" + "=" * 60)
print("7. MOCKING EXTERNAL SERVICES")
print("=" * 60)

# Simulate an API client
class WeatherAPI:
    """Fake weather API client."""
    
    def get_temperature(self, city):
        """Would normally call external API."""
        # In real code, this would make HTTP request
        raise NotImplementedError("Would call external API")


def should_bring_jacket(api, city):
    """Determine if jacket is needed based on temperature."""
    temp = api.get_temperature(city)
    return temp < 60


# Test with mocking
print("--- Mocking External API ---")

# Create mock API
mock_api = Mock(spec=WeatherAPI)

# Cold weather scenario
mock_api.get_temperature.return_value = 45
result = should_bring_jacket(mock_api, "Seattle")
print(f"At 45°F: bring jacket = {result}")

# Warm weather scenario  
mock_api.get_temperature.return_value = 75
result = should_bring_jacket(mock_api, "Miami")
print(f"At 75°F: bring jacket = {result}")

# Verify the mock was called correctly
mock_api.get_temperature.assert_called_with("Miami")


# ============================================================
# 8. SIDE EFFECTS
# ============================================================

print("\n" + "=" * 60)
print("8. SIDE EFFECTS")
print("=" * 60)

mock = Mock()

# Return different values on successive calls
print("--- Successive Return Values ---")
mock.fetch_data.side_effect = ["First call", "Second call", "Third call"]
print(f"Call 1: {mock.fetch_data()}")
print(f"Call 2: {mock.fetch_data()}")
print(f"Call 3: {mock.fetch_data()}")


# Raise exceptions
print("\n--- Raising Exceptions ---")
mock.risky_operation.side_effect = ValueError("Something went wrong")
try:
    mock.risky_operation()
except ValueError as e:
    print(f"Caught expected error: {e}")


# Custom side effect function
print("\n--- Custom Side Effect Function ---")
def validate_input(value):
    if value < 0:
        raise ValueError("Negative values not allowed")
    return value * 2

mock.process.side_effect = validate_input
print(f"process(5) = {mock.process(5)}")
print(f"process(10) = {mock.process(10)}")
try:
    mock.process(-1)
except ValueError as e:
    print(f"process(-1) raised: {e}")


# ============================================================
# 9. TESTING CLASSES
# ============================================================

print("\n" + "=" * 60)
print("9. TESTING CLASSES")
print("=" * 60)

# Class to test
class UserService:
    """User service that depends on a database."""
    
    def __init__(self, database):
        self.database = database
    
    def get_user(self, user_id):
        """Get user from database."""
        return self.database.find_by_id(user_id)
    
    def create_user(self, name, email):
        """Create new user."""
        if self.database.find_by_email(email):
            raise ValueError("Email already exists")
        user = {"name": name, "email": email}
        return self.database.insert(user)
    
    def get_active_users(self):
        """Get all active users."""
        users = self.database.find_all()
        return [u for u in users if u.get("active", False)]


# Test with mock database
class TestUserService(unittest.TestCase):
    
    def setUp(self):
        self.mock_db = Mock()
        self.service = UserService(self.mock_db)
    
    def test_get_user(self):
        """Test getting a user."""
        self.mock_db.find_by_id.return_value = {"id": 1, "name": "John"}
        
        user = self.service.get_user(1)
        
        self.assertEqual(user["name"], "John")
        self.mock_db.find_by_id.assert_called_once_with(1)
    
    def test_create_user_success(self):
        """Test creating a new user."""
        self.mock_db.find_by_email.return_value = None  # Email not taken
        self.mock_db.insert.return_value = {"id": 1, "name": "John", "email": "john@example.com"}
        
        result = self.service.create_user("John", "john@example.com")
        
        self.assertEqual(result["name"], "John")
        self.mock_db.insert.assert_called_once()
    
    def test_create_user_duplicate_email(self):
        """Test that duplicate email raises error."""
        self.mock_db.find_by_email.return_value = {"id": 2}  # Email taken
        
        with self.assertRaises(ValueError) as context:
            self.service.create_user("John", "taken@example.com")
        
        self.assertIn("already exists", str(context.exception))
        self.mock_db.insert.assert_not_called()
    
    def test_get_active_users(self):
        """Test filtering active users."""
        self.mock_db.find_all.return_value = [
            {"id": 1, "name": "Active1", "active": True},
            {"id": 2, "name": "Inactive", "active": False},
            {"id": 3, "name": "Active2", "active": True},
        ]
        
        active = self.service.get_active_users()
        
        self.assertEqual(len(active), 2)
        self.assertEqual(active[0]["name"], "Active1")
        self.assertEqual(active[1]["name"], "Active2")


# Run UserService tests
suite = unittest.TestLoader().loadTestsFromTestCase(TestUserService)
runner = unittest.TextTestRunner(verbosity=2, stream=StringIO())
result = runner.run(suite)
print(f"\nUserService tests: {result.testsRun} run, {len(result.failures)} failed")


# ============================================================
# 10. TEST FIXTURES AND FACTORIES
# ============================================================

print("\n" + "=" * 60)
print("10. TEST FIXTURES AND FACTORIES")
print("=" * 60)

# Simple test data factory
class UserFactory:
    """Factory for creating test users."""
    
    _counter = 0
    
    @classmethod
    def create(cls, **overrides):
        """Create a test user with default or custom values."""
        cls._counter += 1
        defaults = {
            "id": cls._counter,
            "name": f"User {cls._counter}",
            "email": f"user{cls._counter}@example.com",
            "active": True,
            "age": 25,
        }
        defaults.update(overrides)
        return defaults
    
    @classmethod
    def create_batch(cls, count, **overrides):
        """Create multiple test users."""
        return [cls.create(**overrides) for _ in range(count)]
    
    @classmethod
    def reset(cls):
        """Reset the counter."""
        cls._counter = 0


# Using the factory
print("--- Using Test Factory ---")
UserFactory.reset()

user1 = UserFactory.create()
print(f"Default user: {user1}")

user2 = UserFactory.create(name="Custom Name", age=30)
print(f"Custom user: {user2}")

users = UserFactory.create_batch(3, active=False)
print(f"Batch of 3 inactive users: {[u['name'] for u in users]}")


# ============================================================
# 11. PARAMETERIZED TESTS
# ============================================================

print("\n" + "=" * 60)
print("11. PARAMETERIZED TESTS")
print("=" * 60)

# Function to test
def is_palindrome(s):
    """Check if string is a palindrome."""
    s = s.lower().replace(" ", "")
    return s == s[::-1]


# Parameterized test with subTest
class TestPalindrome(unittest.TestCase):
    
    def test_palindromes(self):
        """Test various palindrome cases."""
        test_cases = [
            ("radar", True),
            ("level", True),
            ("A man a plan a canal Panama", True),
            ("hello", False),
            ("world", False),
            ("", True),
            ("a", True),
        ]
        
        for word, expected in test_cases:
            with self.subTest(word=word):
                self.assertEqual(is_palindrome(word), expected)


suite = unittest.TestLoader().loadTestsFromTestCase(TestPalindrome)
runner = unittest.TextTestRunner(verbosity=2, stream=StringIO())
result = runner.run(suite)
print(f"Palindrome tests passed: {result.wasSuccessful()}")


# ============================================================
# SUMMARY
# ============================================================

print("\n" + "=" * 60)
print("TESTING SUMMARY")
print("=" * 60)

print("""
Key Concepts Covered:
āœ“ unittest framework basics
āœ“ Assertion methods (assertEqual, assertTrue, assertRaises, etc.)
āœ“ setUp and tearDown (per test and per class)
āœ“ pytest-style tests with simple assertions
āœ“ Mock objects and MagicMock
āœ“ Patching with @patch decorator and context manager
āœ“ Side effects for mocks
āœ“ Testing classes with mock dependencies
āœ“ Test data factories
āœ“ Parameterized tests with subTest

Testing Best Practices:
1. Test one thing per test
2. Use descriptive test names
3. Follow Arrange-Act-Assert pattern
4. Mock external dependencies
5. Keep tests independent
6. Use factories for test data
7. Aim for good coverage but test quality matters more
""")
Examples - Python Tutorial | DeepML