python
examples
examples.pyšpython
"""
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
""")