🧪 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

PracticeDescriptionExample
Test One ThingEach test should verify one specific behaviortest_deposit_positive_amount()
Descriptive NamesTest names should describe what's being testedtest_withdraw_insufficient_funds()
Arrange-Act-AssertClear test structureSet up → Call function → Check result
Test Edge CasesInclude boundary conditionsZero, negative, very large values
Test Error HandlingVerify errors are raised correctlyassertRaises(ValueError)
Independent TestsTests shouldn't depend on each otherEach 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?

😔Poor
🙁Fair
😊Good
😄Great
🤩Excellent