Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Error Path Testing

🔧 HOW-TO | Learn to test failure scenarios thoroughly

Error paths are where 80% of bugs hide. Chicago TDD emphasizes comprehensive error testing.

Why Error Paths Matter

Most code focuses on the "happy path" (success). Bugs hide in error cases:

#![allow(unused)]
fn main() {
// Happy path is obvious
let parsed = "42".parse::<u32>()?;
assert_eq!(parsed, 42);

// Error path has subtle bugs
// What about "not_a_number"?
// What about negative "-42"?
// What about overflow "99999999999999999999"?
}

Testing Both Paths

Every function should test both success and failure:

#![allow(unused)]
fn main() {
test!(test_complete_behavior, {
    // Success path
    let ok_result = parse_number::<u32>("42");
    assert_ok!(&ok_result);
    assert_eq!(ok_result.unwrap(), 42);

    // Error path
    let err_result = parse_number::<u32>("invalid");
    assert_err!(&err_result);
});
}

Common Error Scenarios

1. Invalid Input

#![allow(unused)]
fn main() {
test!(test_invalid_input, {
    let result = validate_email("not_an_email");
    assert_err!(&result);

    let result = validate_email("");
    assert_err!(&result);

    let result = validate_email("@");
    assert_err!(&result);
});
}

2. Boundary Conditions

#![allow(unused)]
fn main() {
test!(test_boundaries, {
    // Minimum
    assert_ok!(&process(0));

    // Just above minimum
    assert_ok!(&process(1));

    // Maximum valid
    assert_ok!(&process(u32::MAX - 1));

    // Just past maximum
    assert_err!(&process(u32::MAX + 1));  // If checked
});
}

3. Resource Errors

#![allow(unused)]
fn main() {
test!(test_resource_errors, {
    // File doesn't exist
    let result = read_file("nonexistent.txt");
    assert_err!(&result);

    // Permission denied
    let result = write_file("/root/restricted.txt", "data");
    assert_err!(&result);

    // Out of memory (hard to test, but consider it)
});
}

4. State Errors

#![allow(unused)]
fn main() {
test!(test_state_errors, {
    let state = MyStateMachine::new();

    // Valid transition
    assert_ok!(&state.transition_to_active());

    // Invalid transition
    let already_active = MyStateMachine::new().transition_to_active();
    assert_err!(&already_active.transition_to_active());
});
}

Error Messages

Test that error messages are helpful:

#![allow(unused)]
fn main() {
test!(test_error_messages, {
    let result = parse_number::<u32>("not_a_number");

    match result {
        Err(e) => {
            // Verify error message is clear
            assert!(e.to_string().contains("parse error"));
            assert!(e.to_string().contains("not_a_number"));
        }
        Ok(_) => panic!("Should have failed"),
    }
});
}

Error Recovery

Test that code recovers from errors:

#![allow(unused)]
fn main() {
test!(test_error_recovery, {
    // First attempt fails
    let result1 = connect_to_database("invalid_url");
    assert_err!(&result1);

    // Code continues and retries with valid URL
    let result2 = connect_to_database("valid_url");
    assert_ok!(&result2);
});
}

Real-World Example: Form Validation

#![allow(unused)]
fn main() {
test!(test_form_validation, {
    let validator = FormValidator::new();

    // Valid case
    let valid = validator.validate(&FormData {
        username: "alice".to_string(),
        email: "alice@example.com".to_string(),
        password: "secure_password_123".to_string(),
    });
    assert_ok!(&valid);

    // Missing username
    let missing_username = validator.validate(&FormData {
        username: "".to_string(),
        email: "alice@example.com".to_string(),
        password: "secure_password_123".to_string(),
    });
    assert_err!(&missing_username);

    // Invalid email
    let invalid_email = validator.validate(&FormData {
        username: "alice".to_string(),
        email: "not_an_email".to_string(),
        password: "secure_password_123".to_string(),
    });
    assert_err!(&invalid_email);

    // Weak password
    let weak_password = validator.validate(&FormData {
        username: "alice".to_string(),
        email: "alice@example.com".to_string(),
        password: "123".to_string(),  // Too short
    });
    assert_err!(&weak_password);
});
}

Real-World Example: API Endpoint

#![allow(unused)]
fn main() {
test!(test_api_error_handling, {
    let client = ApiClient::new();

    // Success
    let result = client.get_user(123);
    assert_ok!(&result);
    assert_eq!(result.unwrap().id, 123);

    // User not found
    let result = client.get_user(999999);
    assert_err!(&result);

    // Invalid ID
    let result = client.get_user(-1);
    assert_err!(&result);

    // Network error (mock or integration test)
    let result = client.get_user(456);  // Server down
    assert_err!(&result);
});
}

Error Handling Patterns

Pattern: Check and Handle

#![allow(unused)]
fn main() {
test!(test_check_and_handle, {
    let result = risky_operation();

    match result {
        Ok(value) => {
            assert_eq!(value, expected);
        }
        Err(e) => {
            // Handle error
            assert!(e.to_string().len() > 0);
        }
    }
});
}

Pattern: Map Error

#![allow(unused)]
fn main() {
test!(test_map_error, {
    let result = risky_operation()
        .map_err(|e| format!("Operation failed: {}", e));

    assert_err!(&result);
    if let Err(e) = result {
        assert!(e.contains("Operation failed"));
    }
});
}

Pattern: Recover

#![allow(unused)]
fn main() {
test!(test_error_recovery, {
    let result = risky_operation()
        .or_else(|_| fallback_operation());

    // Should succeed via fallback
    assert_ok!(&result);
});
}

Comprehensive Error Test

#![allow(unused)]
fn main() {
test!(test_comprehensive_errors, {
    // Arrange
    let test_cases = vec![
        ("valid_input", true),
        ("", false),
        ("too_long_" /* 100 chars */, false),
        ("special@chars#", false),
        ("123", true),
        ("-123", false),  // Negative not allowed
    ];

    // Act & Assert
    for (input, should_succeed) in test_cases {
        let result = validate_input(input);

        if should_succeed {
            assert_ok!(&result, "Input '{}' should be valid", input);
        } else {
            assert_err!(&result, "Input '{}' should be invalid", input);
        }
    }
});
}

Best Practices

Do:

  • Test both success and error paths
  • Use boundary conditions
  • Test error messages
  • Verify error recovery
  • Document expected errors

Don't:

  • Only test the happy path
  • Assume error handling is correct
  • Ignore boundary conditions
  • Skip error message verification
  • Test implementation details of errors

Error Testing Checklist

For each function, test:

  • Happy path (normal input)
  • Invalid input
  • Boundary conditions (min, max, zero, -1)
  • Empty/null values
  • Resource errors (if applicable)
  • State errors (if applicable)
  • Error messages are clear
  • Error recovery is possible

Common Error Patterns

ScenarioTest Case
Missing inputEmpty string, None, empty Vec
Invalid formatWrong type, malformed data
Out of rangeNegative when only positive allowed
Resource unavailableFile not found, connection refused
State violationInvalid state transition
TimeoutOperation takes too long

Real-World Integration Example

#![allow(unused)]
fn main() {
test!(test_database_operations, {
    let db = Database::new();

    // Success: Insert and retrieve
    let user_id = db.insert_user("alice", "alice@example.com");
    let result = db.get_user(user_id);
    assert_ok!(&result);

    // Error: User not found
    let result = db.get_user(999999);
    assert_err!(&result);

    // Error: Duplicate email
    let result = db.insert_user("bob", "alice@example.com");  // Email taken
    assert_err!(&result);

    // Error: Invalid email
    let result = db.insert_user("carol", "not_an_email");
    assert_err!(&result);
});
}

Next Steps

Learn advanced techniques: Advanced Testing Techniques


Summary

Chicago TDD prioritizes error testing because:

  • Bugs hide in error paths
  • Error handling is often incorrect
  • Users encounter errors in real use

Always test:

  • ✅ Happy path
  • ✅ Error cases
  • ✅ Boundary conditions
  • ✅ Error messages
  • ✅ Recovery