python
exercises
exercises.py🐍python
"""
Testing in Python - Exercises
Practice writing tests, using mocks, and following TDD
"""
# ============================================================
# EXERCISE 1: Basic Test Cases
# ============================================================
"""
Write unit tests for the following StringUtils class.
Cover all methods with at least 2 test cases each.
"""
import unittest
class StringUtils:
"""Utility class for string operations."""
@staticmethod
def reverse(s):
"""Reverse a string."""
return s[::-1]
@staticmethod
def is_palindrome(s):
"""Check if string is a palindrome (case-insensitive)."""
s = s.lower().replace(" ", "")
return s == s[::-1]
@staticmethod
def count_vowels(s):
"""Count vowels in a string."""
return sum(1 for c in s.lower() if c in 'aeiou')
@staticmethod
def truncate(s, max_length, suffix="..."):
"""Truncate string if longer than max_length."""
if len(s) <= max_length:
return s
return s[:max_length - len(suffix)] + suffix
class TestStringUtils(unittest.TestCase):
"""Test cases for StringUtils."""
def test_reverse(self):
# YOUR CODE HERE
pass
def test_is_palindrome(self):
# YOUR CODE HERE
pass
def test_count_vowels(self):
# YOUR CODE HERE
pass
def test_truncate(self):
# YOUR CODE HERE
pass
# ============================================================
# EXERCISE 2: Exception Testing
# ============================================================
"""
Write tests for the BankAccount class that verify exceptions
are raised correctly in error cases.
"""
class BankAccount:
"""Simple bank account with validation."""
def __init__(self, balance=0):
if balance < 0:
raise ValueError("Initial balance cannot be negative")
self._balance = balance
@property
def balance(self):
return self._balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("Deposit amount must be positive")
self._balance += amount
def withdraw(self, amount):
if amount <= 0:
raise ValueError("Withdrawal amount must be positive")
if amount > self._balance:
raise ValueError("Insufficient funds")
self._balance -= amount
class TestBankAccount(unittest.TestCase):
"""Test cases for BankAccount including exception cases."""
def test_create_account_with_positive_balance(self):
# YOUR CODE HERE
pass
def test_create_account_with_negative_balance_raises(self):
# YOUR CODE HERE - use assertRaises
pass
def test_deposit_positive_amount(self):
# YOUR CODE HERE
pass
def test_deposit_negative_amount_raises(self):
# YOUR CODE HERE
pass
def test_withdraw_success(self):
# YOUR CODE HERE
pass
def test_withdraw_insufficient_funds_raises(self):
# YOUR CODE HERE
pass
# ============================================================
# EXERCISE 3: Test with setUp and tearDown
# ============================================================
"""
Write tests for a ShoppingCart class.
Use setUp to create a fresh cart before each test.
"""
class ShoppingCart:
"""Shopping cart for an e-commerce site."""
def __init__(self):
self.items = []
def add_item(self, name, price, quantity=1):
self.items.append({
"name": name,
"price": price,
"quantity": quantity
})
def remove_item(self, name):
self.items = [item for item in self.items if item["name"] != name]
def get_total(self):
return sum(item["price"] * item["quantity"] for item in self.items)
def get_item_count(self):
return sum(item["quantity"] for item in self.items)
def clear(self):
self.items = []
class TestShoppingCart(unittest.TestCase):
"""Test cases for ShoppingCart with setUp."""
def setUp(self):
"""Create a cart with some items before each test."""
# YOUR CODE HERE
pass
def test_add_item(self):
# YOUR CODE HERE
pass
def test_remove_item(self):
# YOUR CODE HERE
pass
def test_get_total(self):
# YOUR CODE HERE
pass
def test_get_item_count(self):
# YOUR CODE HERE
pass
def test_clear(self):
# YOUR CODE HERE
pass
# ============================================================
# EXERCISE 4: Mocking External Dependencies
# ============================================================
"""
Write tests for the EmailNotifier class.
Mock the email_service to avoid sending real emails.
"""
from unittest.mock import Mock, patch
class EmailNotifier:
"""Send email notifications."""
def __init__(self, email_service):
self.email_service = email_service
def send_welcome_email(self, user_email, user_name):
subject = "Welcome!"
body = f"Hello {user_name}, welcome to our platform!"
return self.email_service.send(user_email, subject, body)
def send_password_reset(self, user_email, reset_link):
subject = "Password Reset"
body = f"Click here to reset your password: {reset_link}"
return self.email_service.send(user_email, subject, body)
def send_bulk_newsletter(self, email_list, content):
results = []
for email in email_list:
result = self.email_service.send(email, "Newsletter", content)
results.append(result)
return results
class TestEmailNotifier(unittest.TestCase):
"""Test cases for EmailNotifier with mocked email service."""
def setUp(self):
"""Create mock email service and notifier."""
# YOUR CODE HERE
pass
def test_send_welcome_email(self):
# YOUR CODE HERE
# Verify email_service.send was called with correct arguments
pass
def test_send_password_reset(self):
# YOUR CODE HERE
pass
def test_send_bulk_newsletter(self):
# YOUR CODE HERE
# Verify send was called for each email in the list
pass
# ============================================================
# EXERCISE 5: Test Side Effects
# ============================================================
"""
Test a function that retries on failure using mock side effects.
"""
def retry_fetch(fetcher, max_retries=3):
"""Retry fetching data up to max_retries times."""
last_error: Exception | None = None
for attempt in range(max_retries):
try:
return fetcher.fetch()
except Exception as e:
last_error = e
if last_error is not None:
raise last_error
raise RuntimeError("No attempts made")
class TestRetryFetch(unittest.TestCase):
"""Test retry_fetch with various scenarios."""
def test_succeeds_on_first_try(self):
# YOUR CODE HERE
# Mock fetcher that succeeds immediately
pass
def test_succeeds_after_retries(self):
# YOUR CODE HERE
# Mock fetcher that fails twice then succeeds
# Use side_effect = [Exception, Exception, "success"]
pass
def test_fails_after_max_retries(self):
# YOUR CODE HERE
# Mock fetcher that always fails
pass
# ============================================================
# EXERCISE 6: Parameterized Tests
# ============================================================
"""
Write parameterized tests for a password validator.
Test many cases efficiently using subTest.
"""
import re
def validate_password(password):
"""
Validate password strength.
Returns (is_valid, error_message or None)
Requirements:
- At least 8 characters
- At least one uppercase letter
- At least one lowercase letter
- At least one digit
- At least one special character (!@#$%^&*)
"""
if len(password) < 8:
return False, "Password must be at least 8 characters"
if not re.search(r'[A-Z]', password):
return False, "Password must contain an uppercase letter"
if not re.search(r'[a-z]', password):
return False, "Password must contain a lowercase letter"
if not re.search(r'\d', password):
return False, "Password must contain a digit"
if not re.search(r'[!@#$%^&*]', password):
return False, "Password must contain a special character"
return True, None
class TestPasswordValidator(unittest.TestCase):
"""Parameterized tests for password validator."""
def test_valid_passwords(self):
"""Test various valid passwords."""
valid_passwords = [
"Password1!",
"SecurePass123@",
"MyP@ssw0rd",
# Add more valid passwords
]
for password in valid_passwords:
with self.subTest(password=password):
is_valid, error = validate_password(password)
self.assertTrue(is_valid, f"Expected valid: {password}")
def test_invalid_passwords(self):
"""Test various invalid passwords with expected errors."""
invalid_cases = [
("short1!", "at least 8 characters"),
("nouppercase1!", "uppercase"),
("NOLOWERCASE1!", "lowercase"),
("NoDigits!!", "digit"),
("NoSpecial1a", "special character"),
]
for password, expected_error in invalid_cases:
with self.subTest(password=password):
is_valid, error = validate_password(password)
# YOUR CODE HERE - verify it's invalid and error contains expected text
pass
# ============================================================
# EXERCISE 7: Testing with Files (using tempfile)
# ============================================================
"""
Test a ConfigManager that reads/writes configuration files.
Use tempfile to avoid creating real files.
"""
import tempfile
import os
import json
class ConfigManager:
"""Manage configuration files."""
def __init__(self, config_path):
self.config_path = config_path
self.config = {}
def load(self):
"""Load configuration from file."""
if os.path.exists(self.config_path):
with open(self.config_path, 'r') as f:
self.config = json.load(f)
return self.config
def save(self):
"""Save configuration to file."""
with open(self.config_path, 'w') as f:
json.dump(self.config, f)
def get(self, key, default=None):
"""Get a configuration value."""
return self.config.get(key, default)
def set(self, key, value):
"""Set a configuration value."""
self.config[key] = value
class TestConfigManager(unittest.TestCase):
"""Test ConfigManager using temporary files."""
def setUp(self):
"""Create a temporary file for testing."""
self.temp_file = tempfile.NamedTemporaryFile(
mode='w', suffix='.json', delete=False
)
self.temp_file.close()
self.config_path = self.temp_file.name
def tearDown(self):
"""Clean up temporary file."""
if os.path.exists(self.config_path):
os.unlink(self.config_path)
def test_load_empty_config(self):
# YOUR CODE HERE
pass
def test_save_and_load_config(self):
# YOUR CODE HERE
pass
def test_get_with_default(self):
# YOUR CODE HERE
pass
# ============================================================
# EXERCISE 8: Test a Cache Class
# ============================================================
"""
Implement tests for a simple in-memory cache with TTL.
Test expiration using mocked time.
"""
import time
class SimpleCache:
"""In-memory cache with time-to-live support."""
def __init__(self, default_ttl=60):
self.default_ttl = default_ttl
self._cache = {}
def set(self, key, value, ttl=None):
"""Store a value with optional TTL."""
if ttl is None:
ttl = self.default_ttl
expires_at = time.time() + ttl
self._cache[key] = {"value": value, "expires_at": expires_at}
def get(self, key):
"""Get a value if not expired."""
if key not in self._cache:
return None
entry = self._cache[key]
if time.time() > entry["expires_at"]:
del self._cache[key]
return None
return entry["value"]
def delete(self, key):
"""Delete a key from cache."""
if key in self._cache:
del self._cache[key]
class TestSimpleCache(unittest.TestCase):
"""Test SimpleCache with mocked time."""
def test_set_and_get(self):
# YOUR CODE HERE
pass
def test_get_nonexistent_key(self):
# YOUR CODE HERE
pass
def test_delete(self):
# YOUR CODE HERE
pass
@patch('time.time')
def test_expiration(self, mock_time):
"""Test that cache entries expire correctly."""
# YOUR CODE HERE
# Set mock_time.return_value to control "current time"
# Set a value, advance time past TTL, verify it's gone
pass
# ============================================================
# EXERCISE 9: TDD Exercise - Build a Stack
# ============================================================
"""
Practice Test-Driven Development (TDD):
1. Write tests first
2. Run tests (they fail)
3. Implement minimal code to pass
4. Refactor if needed
Implement a Stack class with these operations:
- push(item): Add item to top
- pop(): Remove and return top item (raise if empty)
- peek(): Return top item without removing (raise if empty)
- is_empty(): Return True if stack is empty
- size(): Return number of items
"""
class TestStack(unittest.TestCase):
"""Tests for Stack class - write these FIRST."""
def test_new_stack_is_empty(self):
# YOUR CODE HERE
pass
def test_push_increases_size(self):
# YOUR CODE HERE
pass
def test_pop_returns_last_pushed(self):
# YOUR CODE HERE
pass
def test_pop_empty_raises_error(self):
# YOUR CODE HERE
pass
def test_peek_returns_without_removing(self):
# YOUR CODE HERE
pass
def test_peek_empty_raises_error(self):
# YOUR CODE HERE
pass
def test_lifo_order(self):
"""Test Last-In-First-Out ordering."""
# YOUR CODE HERE
pass
# Implement Stack class after writing tests
class Stack:
"""Stack data structure - implement after tests."""
def __init__(self):
# YOUR CODE HERE
pass
def push(self, item):
# YOUR CODE HERE
pass
def pop(self):
# YOUR CODE HERE
pass
def peek(self):
# YOUR CODE HERE
pass
def is_empty(self):
# YOUR CODE HERE
pass
def size(self):
# YOUR CODE HERE
pass
# ============================================================
# CHALLENGE: Test an API Client
# ============================================================
"""
Write comprehensive tests for this API client.
Mock all HTTP requests to avoid real network calls.
Test success cases, error handling, and edge cases.
"""
class APIError(Exception):
"""API error with status code."""
def __init__(self, message, status_code):
self.message = message
self.status_code = status_code
super().__init__(message)
class UserAPIClient:
"""Client for user management API."""
def __init__(self, base_url, http_client):
self.base_url = base_url.rstrip('/')
self.http = http_client
def get_user(self, user_id):
"""Get user by ID."""
response = self.http.get(f"{self.base_url}/users/{user_id}")
if response.status_code == 404:
raise APIError("User not found", 404)
if response.status_code != 200:
raise APIError("Failed to get user", response.status_code)
return response.json()
def create_user(self, name, email):
"""Create a new user."""
response = self.http.post(
f"{self.base_url}/users",
json={"name": name, "email": email}
)
if response.status_code == 400:
raise APIError("Invalid user data", 400)
if response.status_code == 409:
raise APIError("Email already exists", 409)
if response.status_code != 201:
raise APIError("Failed to create user", response.status_code)
return response.json()
def list_users(self, page=1, per_page=10):
"""List users with pagination."""
response = self.http.get(
f"{self.base_url}/users",
params={"page": page, "per_page": per_page}
)
if response.status_code != 200:
raise APIError("Failed to list users", response.status_code)
return response.json()
class TestUserAPIClient(unittest.TestCase):
"""Comprehensive tests for UserAPIClient."""
def setUp(self):
self.mock_http = Mock()
self.client = UserAPIClient("https://api.example.com", self.mock_http)
# Implement at least 10 test cases covering:
# - Successful operations
# - Error handling (404, 400, 409, 500)
# - Edge cases (empty responses, pagination)
# YOUR CODE HERE
pass
# ============================================================
# SOLUTIONS (Uncomment to check your work)
# ============================================================
"""
# Solution 1: Basic Test Cases
class TestStringUtils(unittest.TestCase):
def test_reverse(self):
self.assertEqual(StringUtils.reverse("hello"), "olleh")
self.assertEqual(StringUtils.reverse(""), "")
self.assertEqual(StringUtils.reverse("a"), "a")
def test_is_palindrome(self):
self.assertTrue(StringUtils.is_palindrome("radar"))
self.assertTrue(StringUtils.is_palindrome("A man a plan a canal Panama"))
self.assertFalse(StringUtils.is_palindrome("hello"))
def test_count_vowels(self):
self.assertEqual(StringUtils.count_vowels("hello"), 2)
self.assertEqual(StringUtils.count_vowels("rhythm"), 0)
self.assertEqual(StringUtils.count_vowels("AEIOU"), 5)
def test_truncate(self):
self.assertEqual(StringUtils.truncate("hello", 10), "hello")
self.assertEqual(StringUtils.truncate("hello world", 8), "hello...")
self.assertEqual(StringUtils.truncate("hello", 5), "hello")
# Solution 2: Exception Testing
class TestBankAccount(unittest.TestCase):
def test_create_account_with_positive_balance(self):
account = BankAccount(100)
self.assertEqual(account.balance, 100)
def test_create_account_with_negative_balance_raises(self):
with self.assertRaises(ValueError) as context:
BankAccount(-100)
self.assertIn("negative", str(context.exception))
def test_deposit_positive_amount(self):
account = BankAccount(100)
account.deposit(50)
self.assertEqual(account.balance, 150)
def test_deposit_negative_amount_raises(self):
account = BankAccount(100)
with self.assertRaises(ValueError):
account.deposit(-50)
def test_withdraw_success(self):
account = BankAccount(100)
account.withdraw(30)
self.assertEqual(account.balance, 70)
def test_withdraw_insufficient_funds_raises(self):
account = BankAccount(100)
with self.assertRaises(ValueError) as context:
account.withdraw(150)
self.assertIn("Insufficient", str(context.exception))
# Solution 3: ShoppingCart Tests
class TestShoppingCart(unittest.TestCase):
def setUp(self):
self.cart = ShoppingCart()
self.cart.add_item("Apple", 1.50, 2)
self.cart.add_item("Banana", 0.75, 3)
def test_add_item(self):
self.cart.add_item("Orange", 2.00, 1)
self.assertEqual(len(self.cart.items), 3)
def test_remove_item(self):
self.cart.remove_item("Apple")
self.assertEqual(len(self.cart.items), 1)
def test_get_total(self):
# (1.50 * 2) + (0.75 * 3) = 3.00 + 2.25 = 5.25
self.assertEqual(self.cart.get_total(), 5.25)
def test_get_item_count(self):
self.assertEqual(self.cart.get_item_count(), 5) # 2 + 3
def test_clear(self):
self.cart.clear()
self.assertEqual(len(self.cart.items), 0)
# Solution 4: Mocking EmailNotifier
class TestEmailNotifier(unittest.TestCase):
def setUp(self):
self.mock_email = Mock()
self.notifier = EmailNotifier(self.mock_email)
def test_send_welcome_email(self):
self.mock_email.send.return_value = True
result = self.notifier.send_welcome_email("john@example.com", "John")
self.assertTrue(result)
self.mock_email.send.assert_called_once_with(
"john@example.com",
"Welcome!",
"Hello John, welcome to our platform!"
)
def test_send_password_reset(self):
self.mock_email.send.return_value = True
result = self.notifier.send_password_reset("john@example.com", "http://reset.link")
self.mock_email.send.assert_called_once()
args = self.mock_email.send.call_args[0]
self.assertIn("reset.link", args[2])
def test_send_bulk_newsletter(self):
self.mock_email.send.return_value = True
emails = ["a@test.com", "b@test.com", "c@test.com"]
results = self.notifier.send_bulk_newsletter(emails, "Newsletter content")
self.assertEqual(len(results), 3)
self.assertEqual(self.mock_email.send.call_count, 3)
# Solution 5: Retry Fetch
class TestRetryFetch(unittest.TestCase):
def test_succeeds_on_first_try(self):
mock_fetcher = Mock()
mock_fetcher.fetch.return_value = "data"
result = retry_fetch(mock_fetcher)
self.assertEqual(result, "data")
self.assertEqual(mock_fetcher.fetch.call_count, 1)
def test_succeeds_after_retries(self):
mock_fetcher = Mock()
mock_fetcher.fetch.side_effect = [Exception("fail"), Exception("fail"), "success"]
result = retry_fetch(mock_fetcher)
self.assertEqual(result, "success")
self.assertEqual(mock_fetcher.fetch.call_count, 3)
def test_fails_after_max_retries(self):
mock_fetcher = Mock()
mock_fetcher.fetch.side_effect = Exception("always fails")
with self.assertRaises(Exception):
retry_fetch(mock_fetcher, max_retries=3)
self.assertEqual(mock_fetcher.fetch.call_count, 3)
# Solution 9: Stack (TDD)
class Stack:
def __init__(self):
self._items = []
def push(self, item):
self._items.append(item)
def pop(self):
if self.is_empty():
raise IndexError("Stack is empty")
return self._items.pop()
def peek(self):
if self.is_empty():
raise IndexError("Stack is empty")
return self._items[-1]
def is_empty(self):
return len(self._items) == 0
def size(self):
return len(self._items)
class TestStack(unittest.TestCase):
def test_new_stack_is_empty(self):
stack = Stack()
self.assertTrue(stack.is_empty())
self.assertEqual(stack.size(), 0)
def test_push_increases_size(self):
stack = Stack()
stack.push(1)
self.assertEqual(stack.size(), 1)
self.assertFalse(stack.is_empty())
def test_pop_returns_last_pushed(self):
stack = Stack()
stack.push("a")
self.assertEqual(stack.pop(), "a")
def test_pop_empty_raises_error(self):
stack = Stack()
with self.assertRaises(IndexError):
stack.pop()
def test_peek_returns_without_removing(self):
stack = Stack()
stack.push(1)
self.assertEqual(stack.peek(), 1)
self.assertEqual(stack.size(), 1)
def test_peek_empty_raises_error(self):
stack = Stack()
with self.assertRaises(IndexError):
stack.peek()
def test_lifo_order(self):
stack = Stack()
stack.push(1)
stack.push(2)
stack.push(3)
self.assertEqual(stack.pop(), 3)
self.assertEqual(stack.pop(), 2)
self.assertEqual(stack.pop(), 1)
"""
if __name__ == "__main__":
print("Testing Exercises")
print("=" * 50)
print("\nComplete the exercises above to practice:")
print("- Writing basic unit tests")
print("- Testing exceptions")
print("- Using setUp and tearDown")
print("- Mocking external dependencies")
print("- Testing with side effects")
print("- Parameterized testing")
print("- Testing file operations")
print("- Test-Driven Development (TDD)")
print("\nRun with: python -m unittest exercises.py")
print("Or with pytest: pytest exercises.py -v")