Testing Edge Cases

Chapter 5: Testing Edge Cases and Error Conditions

Most bugs hide at the edges. Your function works perfectly for typical inputs but crashes on empty lists, negative numbers, or None values. Testing edge cases—the boundary conditions and unusual inputs—separates reliable code from brittle code. This chapter teaches you to identify, test, and handle edge cases systematically.

What Are Edge Cases?

Edge cases are inputs at the extreme ends of acceptable ranges or unusual combinations that developers often forget to handle. They're called "edge" because they sit at the boundaries of normal operation.

Common Edge Cases:

  • Empty collections: Empty lists, dictionaries, strings
  • Boundary values: Zero, negative numbers, maximum integers
  • None/null values: Missing or undefined data
  • Type mismatches: String when expecting integer
  • Special characters: Unicode, newlines, escape sequences

Why edge cases matter: Production systems encounter these constantly. Users submit empty forms. APIs return null. Files are empty. If you don't test these cases, users will find the bugs for you.

Boundary Value Testing

Boundary value analysis tests values at the edges of acceptable ranges. If a function accepts integers 1-100, test 0, 1, 100, and 101. Bugs cluster at boundaries.

The Rule of Boundaries: Test the minimum, minimum+1, maximum-1, and maximum. Also test just outside these ranges.

Notice the tests at 64, 65, and above. The boundary is 65—that's where behavior changes. That's where bugs hide.

Handling None and Empty Values

None values cause more bugs than almost anything else. APIs return None. Database queries return None. User input might be None. Test for it explicitly.

Empty vs None: Empty and None are different. An empty list [] is not the same as None. Empty means "container with nothing in it." None means "no container at all."

Defensive Programming: Check for None before using values. Decide what None means in your context—error or default value?

Empty String Edge Cases:

def format_greeting(name):
    """Create greeting message."""
    if not name:  # Catches None and empty string
        return "Hello, stranger!"
    return f"Hello, {name}!"

def test_greeting_with_none():
    assert format_greeting(None) == "Hello, stranger!"

def test_greeting_with_empty_string():
    assert format_greeting("") == "Hello, stranger!"

def test_greeting_with_whitespace():
    # Does whitespace-only count as empty?
    assert format_greeting("   ") == "Hello,    !"  # Might want to .strip()

Testing Error Conditions

Good code fails gracefully. Test that your code handles errors appropriately—raises the right exceptions, returns error codes, logs properly.

Using pytest.raises:

import pytest

def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b

def test_divide_by_zero():
    with pytest.raises(ValueError, match="Cannot divide by zero"):
        divide(10, 0)

def test_divide_normal():
    assert divide(10, 2) == 5

Test Error Messages: Don't just check that an exception is raised. Verify the message is helpful. Good error messages help users fix problems.

Multiple Error Conditions:

def create_user(username, age):
    if not username:
        raise ValueError("Username required")
    if age < 0:
        raise ValueError("Age must be positive")
    if age > 150:
        raise ValueError("Invalid age")
    return {"username": username, "age": age}

def test_username_required():
    with pytest.raises(ValueError, match="Username required"):
        create_user("", 25)

def test_negative_age():
    with pytest.raises(ValueError, match="positive"):
        create_user("alice", -5)

def test_unrealistic_age():
    with pytest.raises(ValueError, match="Invalid"):
        create_user("alice", 200)

Type Edge Cases

Python is dynamically typed. Users can pass anything. What happens when someone passes a string to a function expecting an integer?

Decide Type Handling Strategy:

  1. Duck typing: "If it walks like a duck..." Try to use it, fail if it doesn't work
  2. Explicit validation: Check types upfront, raise errors for wrong types
  3. Coercion: Convert to expected type if possible
def calculate_total(prices):
    """Sum prices. What if prices isn't a list?"""
    return sum(prices)

# Tests for different types
def test_with_list():
    assert calculate_total([1, 2, 3]) == 6

def test_with_tuple():
    # Duck typing: tuples work too
    assert calculate_total((1, 2, 3)) == 6

def test_with_generator():
    # Generators work
    assert calculate_total(x for x in [1, 2, 3]) == 6

def test_with_none():
    # This will fail - decide if that's acceptable
    with pytest.raises(TypeError):
        calculate_total(None)

Special Character Edge Cases

Strings with newlines, quotes, Unicode, or special characters often break string processing. Test them explicitly.

Common String Edge Cases:

  • Newlines: "Hello\nWorld"
  • Tabs and whitespace: " \t "
  • Quotes: 'O"Reilly' and "She said 'hi'"
  • Unicode: "Café", "北京", emoji
  • Very long strings: 10,000 characters
  • Special characters: "&lt;script&gt;alert(&#039;xss&#039;)&lt;/script&gt;"
def sanitize_input(text):
    """Remove dangerous characters."""
    if not text:
        return ""
    return text.replace("<", "&lt;").replace(">", "&gt;")

def test_sanitize_script_tags():
    dirty = "&lt;script&gt;alert(&#039;xss&#039;)&lt;/script&gt;"
    clean = sanitize_input(dirty)
    assert "<script>" not in clean
    assert "&lt;script&gt;" in clean

def test_sanitize_unicode():
    text = "Café北京"
    assert sanitize_input(text) == text  # Should preserve

def test_sanitize_empty():
    assert sanitize_input("") == ""

def test_sanitize_none():
    assert sanitize_input(None) == ""

Systematic Edge Case Testing

Checklist for Edge Cases:

  1. Empty inputs: [], {}, "", None
  2. Boundary values: 0, -1, MAX_INT, MIN_INT
  3. Type variations: What if int is passed as string?
  4. Error conditions: Invalid data, missing files, network failures
  5. Special characters: Unicode, newlines, escape sequences
  6. Large inputs: 1 million items, 10MB strings
  7. Concurrent access: What if called simultaneously?

Don't test every possible edge case—that's impossible. Identify the risky ones based on how your code is used.

Common Edge Case Patterns

Pattern 1: The Empty Input Check

Always test what happens with empty inputs. This catches a huge class of bugs.

def process_items(items):
    if not items:  # Handles None, [], "", etc.
        return []
    return [process_item(item) for item in items]

Pattern 2: The Null Object Pattern

Instead of checking for None everywhere, return empty but valid objects.

def get_user_preferences(user_id):
    prefs = database.get_preferences(user_id)
    if prefs is None:
        return {}  # Empty dict, not None
    return prefs

Callers can safely use prefs.get('theme') without checking for None first.

Pattern 3: The Validation Guard

Validate inputs early and raise clear errors. Don't let bad data propagate through your system.

def transfer_money(from_account, to_account, amount):
    if amount <= 0:
        raise ValueError("Amount must be positive")
    if from_account == to_account:
        raise ValueError("Cannot transfer to same account")
    # Now proceed safely

Pattern 4: The Type Coercion

When accepting flexible input types, convert to expected type explicitly.

def calculate_tax(price, rate):
    # Accept strings or numbers
    price = float(price)
    rate = float(rate)

    if rate < 0 or rate > 1:
        raise ValueError("Rate must be between 0 and 1")

    return price * rate

These common patterns prevent most edge case bugs in production systems. Apply them consistently throughout your codebase and your code becomes significantly more robust and reliable.

Course Recommendations

Advanced Python Testing

  • Comprehensive edge case strategies
  • Property-based testing for edge case discovery
  • Real-world debugging techniques
  • Enroll at paiml.com

Defensive Programming in Python

  • Input validation patterns
  • Error handling best practices
  • Type checking strategies
  • Enroll at paiml.com

Test-Driven Development Mastery

Quiz

📝 Test Your Knowledge: Testing Edge Cases

Take this quiz to reinforce what you've learned in this chapter.