🧪 Write Unit Tests
Unit tests verify that individual parts of your code work correctly. They help catch bugs early, make refactoring safer, and serve as documentation for how your code should behave. Good tests give you confidence in your code.
import unittest
# Example code to test
def calculate_area(length, width):
"""Calculate the area of a rectangle."""
if length < 0 or width < 0:
raise ValueError("Dimensions must be positive")
return length * width
def is_even(number):
"""Check if a number is even."""
return number % 2 == 0
def format_name(first_name, last_name):
"""Format a full name properly."""
if not first_name or not last_name:
raise ValueError("Both first and last name required")
return f"{first_name.strip().title()} {last_name.strip().title()}"
# Unit tests for the functions
class TestCalculations(unittest.TestCase):
"""Test mathematical calculations."""
def test_calculate_area_positive_numbers(self):
"""Test area calculation with positive numbers."""
result = calculate_area(5, 3)
self.assertEqual(result, 15)
def test_calculate_area_zero(self):
"""Test area calculation with zero."""
result = calculate_area(0, 5)
self.assertEqual(result, 0)
def test_calculate_area_negative_raises_error(self):
"""Test that negative dimensions raise an error."""
with self.assertRaises(ValueError):
calculate_area(-5, 3)
def test_is_even_with_even_number(self):
"""Test is_even with even numbers."""
self.assertTrue(is_even(4))
self.assertTrue(is_even(0))
self.assertTrue(is_even(-2))
def test_is_even_with_odd_number(self):
"""Test is_even with odd numbers."""
self.assertFalse(is_even(3))
self.assertFalse(is_even(1))
self.assertFalse(is_even(-1))
class TestStringFormatting(unittest.TestCase):
"""Test string formatting functions."""
def test_format_name_normal_case(self):
"""Test name formatting with normal input."""
result = format_name("john", "doe")
self.assertEqual(result, "John Doe")
def test_format_name_with_spaces(self):
"""Test name formatting with extra spaces."""
result = format_name(" alice ", " smith ")
self.assertEqual(result, "Alice Smith")
def test_format_name_empty_first_name(self):
"""Test that empty first name raises error."""
with self.assertRaises(ValueError):
format_name("", "doe")
def test_format_name_empty_last_name(self):
"""Test that empty last name raises error."""
with self.assertRaises(ValueError):
format_name("john", "")
# Run the tests
if __name__ == "__main__":
unittest.main()
🎯 Understanding Unit Tests
Unit tests are small, focused tests that verify individual functions or methods work correctly.
Basic Test Structure
import unittest
# Simple functions to demonstrate testing
def add_numbers(a, b):
"""Add two numbers together."""
return a + b
def divide_numbers(a, b):
"""Divide first number by second number."""
if b == 0:
raise ZeroDivisionError("Cannot divide by zero")
return a / b
def get_grade(score):
"""Convert numeric score to letter grade."""
if score < 0 or score > 100:
raise ValueError("Score must be between 0 and 100")
if score >= 90:
return "A"
elif score >= 80:
return "B"
elif score >= 70:
return "C"
elif score >= 60:
return "D"
else:
return "F"
class TestBasicOperations(unittest.TestCase):
"""Basic unit test examples."""
def test_add_positive_numbers(self):
"""Test adding positive numbers."""
# Arrange
a, b = 3, 5
# Act
result = add_numbers(a, b)
# Assert
self.assertEqual(result, 8)
def test_add_negative_numbers(self):
"""Test adding negative numbers."""
result = add_numbers(-2, -3)
self.assertEqual(result, -5)
def test_divide_normal_case(self):
"""Test normal division."""
result = divide_numbers(10, 2)
self.assertEqual(result, 5.0)
def test_divide_by_zero_raises_error(self):
"""Test that dividing by zero raises an error."""
with self.assertRaises(ZeroDivisionError):
divide_numbers(10, 0)
def test_grade_a_range(self):
"""Test A grade assignments."""
self.assertEqual(get_grade(95), "A")
self.assertEqual(get_grade(90), "A")
self.assertEqual(get_grade(100), "A")
def test_grade_boundary_cases(self):
"""Test grade boundaries."""
self.assertEqual(get_grade(89), "B") # Just below A
self.assertEqual(get_grade(90), "A") # Just at A
self.assertEqual(get_grade(79), "C") # Just below B
self.assertEqual(get_grade(80), "B") # Just at B
def test_grade_invalid_score(self):
"""Test invalid scores raise errors."""
with self.assertRaises(ValueError):
get_grade(-5)
with self.assertRaises(ValueError):
get_grade(105)
# Manual test runner for demonstration
def run_simple_tests():
"""Run tests manually for demonstration."""
print("Running Unit Tests:")
print("=" * 40)
# Test 1: Addition
try:
assert add_numbers(3, 5) == 8
print("✓ Addition test passed")
except AssertionError:
print("✗ Addition test failed")
# Test 2: Division
try:
assert divide_numbers(10, 2) == 5.0
print("✓ Division test passed")
except AssertionError:
print("✗ Division test failed")
# Test 3: Error handling
try:
divide_numbers(10, 0)
print("✗ Error handling test failed - should have raised error")
except ZeroDivisionError:
print("✓ Error handling test passed")
# Test 4: Grade calculation
try:
assert get_grade(95) == "A"
assert get_grade(85) == "B"
assert get_grade(75) == "C"
print("✓ Grade calculation tests passed")
except AssertionError:
print("✗ Grade calculation tests failed")
run_simple_tests()
Testing Classes and Objects
import unittest
# Example class to test
class BankAccount:
"""Simple bank account for testing."""
def __init__(self, initial_balance=0):
if initial_balance < 0:
raise ValueError("Initial balance cannot be negative")
self._balance = initial_balance
self._transactions = []
@property
def balance(self):
"""Get current balance."""
return self._balance
def deposit(self, amount):
"""Deposit money to account."""
if amount <= 0:
raise ValueError("Deposit amount must be positive")
self._balance += amount
self._transactions.append(f"Deposited ${amount}")
return self._balance
def withdraw(self, amount):
"""Withdraw money from account."""
if amount <= 0:
raise ValueError("Withdrawal amount must be positive")
if amount > self._balance:
raise ValueError("Insufficient funds")
self._balance -= amount
self._transactions.append(f"Withdrew ${amount}")
return self._balance
def get_transaction_count(self):
"""Get number of transactions."""
return len(self._transactions)
class TestBankAccount(unittest.TestCase):
"""Test the BankAccount class."""
def setUp(self):
"""Set up test fixtures before each test method."""
self.account = BankAccount(100) # Start with $100
def test_initial_balance(self):
"""Test account creation with initial balance."""
new_account = BankAccount(50)
self.assertEqual(new_account.balance, 50)
def test_initial_balance_zero(self):
"""Test account creation with zero balance."""
new_account = BankAccount()
self.assertEqual(new_account.balance, 0)
def test_negative_initial_balance_raises_error(self):
"""Test that negative initial balance raises error."""
with self.assertRaises(ValueError):
BankAccount(-10)
def test_deposit_positive_amount(self):
"""Test depositing positive amount."""
new_balance = self.account.deposit(50)
self.assertEqual(new_balance, 150)
self.assertEqual(self.account.balance, 150)
def test_deposit_zero_raises_error(self):
"""Test that depositing zero raises error."""
with self.assertRaises(ValueError):
self.account.deposit(0)
def test_deposit_negative_raises_error(self):
"""Test that depositing negative amount raises error."""
with self.assertRaises(ValueError):
self.account.deposit(-25)
def test_withdraw_valid_amount(self):
"""Test withdrawing valid amount."""
new_balance = self.account.withdraw(30)
self.assertEqual(new_balance, 70)
self.assertEqual(self.account.balance, 70)
def test_withdraw_insufficient_funds(self):
"""Test withdrawing more than balance."""
with self.assertRaises(ValueError):
self.account.withdraw(150) # More than $100 balance
def test_withdraw_negative_amount(self):
"""Test withdrawing negative amount."""
with self.assertRaises(ValueError):
self.account.withdraw(-10)
def test_multiple_transactions(self):
"""Test multiple deposits and withdrawals."""
self.account.deposit(25) # Balance: 125
self.account.withdraw(50) # Balance: 75
self.account.deposit(100) # Balance: 175
self.assertEqual(self.account.balance, 175)
self.assertEqual(self.account.get_transaction_count(), 3)
def tearDown(self):
"""Clean up after each test method."""
# In this simple case, nothing to clean up
# But this is where you'd close files, database connections, etc.
pass
class TestEdgeCases(unittest.TestCase):
"""Test edge cases and boundary conditions."""
def test_large_amounts(self):
"""Test with large monetary amounts."""
account = BankAccount(1000000) # $1 million
account.deposit(500000) # Add $500k
self.assertEqual(account.balance, 1500000)
def test_precise_decimal_amounts(self):
"""Test with decimal amounts."""
account = BankAccount(100.50)
account.deposit(25.25)
account.withdraw(50.75)
# Use assertAlmostEqual for floating point comparisons
self.assertAlmostEqual(account.balance, 75.00, places=2)
def test_empty_account_operations(self):
"""Test operations on empty account."""
account = BankAccount() # $0 balance
# Should be able to deposit
account.deposit(50)
self.assertEqual(account.balance, 50)
# Should be able to withdraw all
account.withdraw(50)
self.assertEqual(account.balance, 0)
# Simple test runner
def demonstrate_testing():
"""Demonstrate running tests."""
print("Bank Account Testing Examples:")
print("=" * 50)
# Create test account
account = BankAccount(100)
# Test deposit
try:
account.deposit(50)
assert account.balance == 150
print("✓ Deposit test passed")
except Exception as e:
print(f"✗ Deposit test failed: {e}")
# Test withdrawal
try:
account.withdraw(25)
assert account.balance == 125
print("✓ Withdrawal test passed")
except Exception as e:
print(f"✗ Withdrawal test failed: {e}")
# Test error conditions
try:
account.withdraw(200) # Should fail
print("✗ Insufficient funds test failed - should have raised error")
except ValueError:
print("✓ Insufficient funds test passed")
except Exception as e:
print(f"✗ Unexpected error: {e}")
demonstrate_testing()
📋 Testing Best Practices
Practice | Description | Example |
---|---|---|
Test One Thing | Each test should verify one specific behavior | test_deposit_positive_amount() |
Descriptive Names | Test names should describe what's being tested | test_withdraw_insufficient_funds() |
Arrange-Act-Assert | Clear test structure | Set up → Call function → Check result |
Test Edge Cases | Include boundary conditions | Zero, negative, very large values |
Test Error Handling | Verify errors are raised correctly | assertRaises(ValueError) |
Independent Tests | Tests shouldn't depend on each other | Each test can run alone |
🔧 Common Test Patterns
Testing with Sample Data
import unittest
# Function that processes lists of data
def calculate_statistics(numbers):
"""Calculate basic statistics for a list of numbers."""
if not numbers:
return {
'count': 0,
'sum': 0,
'average': 0,
'min': None,
'max': None
}
return {
'count': len(numbers),
'sum': sum(numbers),
'average': sum(numbers) / len(numbers),
'min': min(numbers),
'max': max(numbers)
}
def filter_even_numbers(numbers):
"""Filter out odd numbers, keeping only even ones."""
return [num for num in numbers if num % 2 == 0]
def find_common_items(list1, list2):
"""Find items that appear in both lists."""
return list(set(list1) & set(list2))
class TestDataProcessing(unittest.TestCase):
"""Test data processing functions."""
def setUp(self):
"""Set up test data."""
self.sample_numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
self.mixed_numbers = [-5, 0, 3, 10, -2, 8]
self.empty_list = []
def test_statistics_normal_data(self):
"""Test statistics with normal data."""
result = calculate_statistics(self.sample_numbers)
self.assertEqual(result['count'], 10)
self.assertEqual(result['sum'], 55)
self.assertEqual(result['average'], 5.5)
self.assertEqual(result['min'], 1)
self.assertEqual(result['max'], 10)
def test_statistics_empty_list(self):
"""Test statistics with empty list."""
result = calculate_statistics(self.empty_list)
self.assertEqual(result['count'], 0)
self.assertEqual(result['sum'], 0)
self.assertEqual(result['average'], 0)
self.assertIsNone(result['min'])
self.assertIsNone(result['max'])
def test_statistics_single_item(self):
"""Test statistics with single item."""
result = calculate_statistics([42])
self.assertEqual(result['count'], 1)
self.assertEqual(result['sum'], 42)
self.assertEqual(result['average'], 42)
self.assertEqual(result['min'], 42)
self.assertEqual(result['max'], 42)
def test_filter_even_numbers(self):
"""Test filtering even numbers."""
result = filter_even_numbers(self.sample_numbers)
expected = [2, 4, 6, 8, 10]
self.assertEqual(result, expected)
def test_filter_even_numbers_mixed(self):
"""Test filtering with negative and zero."""
result = filter_even_numbers(self.mixed_numbers)
expected = [0, 10, -2, 8]
self.assertEqual(result, expected)
def test_find_common_items(self):
"""Test finding common items."""
list1 = [1, 2, 3, 4, 5]
list2 = [4, 5, 6, 7, 8]
result = find_common_items(list1, list2)
# Sort both lists since order might vary
self.assertEqual(sorted(result), [4, 5])
def test_find_common_items_no_overlap(self):
"""Test finding common items with no overlap."""
list1 = [1, 2, 3]
list2 = [4, 5, 6]
result = find_common_items(list1, list2)
self.assertEqual(result, [])
# Demonstrate running specific tests
def run_sample_tests():
"""Run some tests manually to show results."""
print("\nData Processing Tests:")
print("=" * 40)
# Test statistics
numbers = [2, 4, 6, 8, 10]
stats = calculate_statistics(numbers)
expected_sum = 30
expected_avg = 6.0
if stats['sum'] == expected_sum and stats['average'] == expected_avg:
print("✓ Statistics calculation test passed")
else:
print("✗ Statistics calculation test failed")
# Test filtering
all_numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
even_numbers = filter_even_numbers(all_numbers)
expected_evens = [2, 4, 6, 8, 10]
if even_numbers == expected_evens:
print("✓ Even number filtering test passed")
else:
print("✗ Even number filtering test failed")
# Test common items
list_a = [1, 2, 3, 4]
list_b = [3, 4, 5, 6]
common = find_common_items(list_a, list_b)
if sorted(common) == [3, 4]:
print("✓ Common items test passed")
else:
print("✗ Common items test failed")
run_sample_tests()
🎯 Key Takeaways
🚀 What's Next?
Learn how to document your code effectively with clear comments and helpful documentation.
Continue to: Document Your Code
Was this helpful?
Track Your Learning Progress
Sign in to bookmark tutorials and keep track of your learning journey.
Your progress is saved automatically as you read.