AI Tools Compared

Writing test cases for edge conditions takes time and careful thought. pytest’s parametrize decorator makes it efficient to run the same test against multiple inputs, but figuring out which edge cases to cover requires understanding your code’s boundary conditions. AI coding assistants can help you identify these cases and generate the parametrize code automatically.

What is pytest Parametrize?

pytest parametrize allows you to run a test function multiple times with different arguments. Instead of writing separate test functions for each case, you define the test once and specify the different inputs:

import pytest

@pytest.mark.parametrize("input,expected", [
    (2, 4),
    (3, 9),
    (0, 0),
])
def test_square(input, expected):
    assert input ** 2 == expected

When you run this test, pytest executes test_square three times—one for each parameter set. This approach keeps your test file clean while ensuring thorough coverage.

How AI Helps Identify Edge Cases

AI tools analyze your function’s logic and suggest boundary conditions you might overlook. The most common edge cases include:

When you ask an AI assistant to generate parametrize test cases, provide the function signature, its purpose, and any documented constraints. The AI then produces test parameters covering these scenarios.

Practical Example: String Processing Function

Consider a function that validates email addresses:

def validate_email(email: str) -> bool:
    """Validate email format."""
    if not email or "@" not in email:
        return False
    local, domain = email.split("@")
    return bool(local and domain and "." in domain)

Manually identifying all edge cases takes effort. Here’s how to prompt an AI assistant effectively:

“Generate pytest parametrize test cases for this validate_email function. Cover: empty strings, None, valid emails, missing @ symbol, missing domain, missing local part, single-character local and domain, emails with dots in local part, and unicode characters.”

The AI produces test parameters:

@pytest.mark.parametrize("email,expected", [
    ("", False),
    (None, False),
    ("test@example.com", True),
    ("test@com", False),
    ("@example.com", False),
    ("test@", False),
    ("a@b.c", True),
    ("t@t.t", True),
    ("test.user@example.com", True),
])
def test_validate_email(email, expected):
    assert validate_email(email) == expected

This covers the obvious cases plus subtle boundaries like single-character inputs.

Testing Numerical Functions

Numerical functions often have tricky edge cases around zero, negative values, and overflow. For a division function:

def divide(a: float, b: float) -> float:
    """Divide a by b."""
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b

Prompt the AI to generate edge cases:

“Write pytest parametrize test cases for this divide function. Include: normal division, division by zero (expect ValueError), zero divided by number, negative numbers, very large numbers, floating-point precision cases.”

The generated tests:

import pytest

@pytest.mark.parametrize("a,b,expected", [
    (10, 2, 5.0),
    (10, 3, 3.3333333333333335),
    (0, 5, 0.0),
    (-10, 2, -5.0),
    (10, -2, -5.0),
    (-10, -2, 5.0),
    (1e308, 1e-308, 1e308),  # Large numbers
    (0.1 + 0.2, 0.3, False),  # Float precision edge case
])
def test_divide(a, b, expected):
    if b == 0:
        with pytest.raises(ValueError):
            divide(a, b)
    elif isinstance(expected, bool) and not expected:
        # Float precision case - check approximately
        assert abs(divide(a, b) - expected) < 1e-10
    else:
        assert divide(a, b) == expected

Testing List and Collection Functions

List processing functions need edge case testing for empty lists, single elements, and duplicates:

def find_unique(items: list) -> list:
    """Return unique items while preserving order."""
    seen = set()
    unique = []
    for item in items:
        if item not in seen:
            seen.add(item)
            unique.append(item)
    return unique

AI-generated test parameters:

@pytest.mark.parametrize("items,expected", [
    ([], []),
    ([1], [1]),
    ([1, 2, 3], [1, 2, 3]),
    ([1, 1, 1], [1]),
    ([1, 2, 1, 2], [1, 2]),
    ([None, None], [None]),
    ([0, 0, 0], [0]),
    (["a", "b", "a"], ["a", "b"]),
])
def test_find_unique(items, expected):
    assert find_unique(items) == expected

Notice how the AI identifies None, zero, and empty string as distinct edge cases—all important for thorough testing.

Crafting Prompts That Get Better Results

The quality of AI-generated test cases depends heavily on how you phrase your prompt. Vague prompts produce generic tests; specific prompts produce thorough parametrize sets tailored to your function’s logic.

A weak prompt: “Write tests for my sort function.”

A stronger prompt: “Write pytest parametrize test cases for this sort function that accepts a list of integers. It should handle: an empty list, a single-element list, a list already in ascending order, a list in descending order, a list with duplicate values, a list with negative numbers mixed with positive, and a list containing the same value repeated. For each case, show the input list and the expected sorted output.”

The stronger prompt specifies the function type, the domain of inputs, and the categories of edge cases. The AI can generate meaningful parameters rather than guessing what matters.

When working on string functions, explicitly mention: encoding issues, leading/trailing whitespace, mixed case, and strings containing only whitespace. When working on date/time functions, mention: timezone boundaries, leap year handling, month-end dates, and Unix epoch zero. Domain knowledge embedded in your prompt directly improves coverage.

Using AI to Test Exception Handling

Many functions raise exceptions under certain conditions. AI assistants can generate parametrize tests that verify both the happy path and error conditions in the same test function using pytest.raises as a context manager.

def parse_age(value: str) -> int:
    """Parse age from string. Raises ValueError for invalid input."""
    age = int(value)
    if age < 0 or age > 150:
        raise ValueError(f"Age {age} out of valid range")
    return age

Prompt the AI: “Write parametrize tests for parse_age. Cover: valid ages like 0, 1, 99, 150; invalid strings like ‘abc’, ‘’, ‘-‘; out-of-range integers like -1 and 151; edge values at the boundary.”

AI-generated output:

import pytest

@pytest.mark.parametrize("value,expected_result,raises", [
    ("0", 0, None),
    ("1", 1, None),
    ("99", 99, None),
    ("150", 150, None),
    ("abc", None, ValueError),
    ("", None, ValueError),
    ("-1", None, ValueError),
    ("151", None, ValueError),
    ("-0", 0, None),
])
def test_parse_age(value, expected_result, raises):
    if raises:
        with pytest.raises(raises):
            parse_age(value)
    else:
        assert parse_age(value) == expected_result

This pattern—using a raises parameter to control whether the test expects an exception—is a clean way to test both success and failure paths without duplicating test logic.

Parametrize IDs for Readable Test Output

When you run parametrize tests, pytest generates IDs like test_validate_email[email0-expected0] by default. AI assistants can add descriptive IDs that make test failures easier to diagnose.

@pytest.mark.parametrize("email,expected", [
    pytest.param("", False, id="empty_string"),
    pytest.param(None, False, id="none_input"),
    pytest.param("test@example.com", True, id="valid_email"),
    pytest.param("@example.com", False, id="missing_local_part"),
    pytest.param("test@", False, id="missing_domain"),
])
def test_validate_email_with_ids(email, expected):
    assert validate_email(email) == expected

When a test fails, the output now reads FAILED test_validate_email_with_ids[missing_local_part] instead of a numeric index — immediately identifying the problem without needing to inspect parameter lists. Ask the AI to add id= arguments whenever generating parametrize cases.

Best Practices for AI-Generated Tests

AI assistants are helpful but review their output. Follow these practices:

Provide complete function context: Include the full function signature, type hints, and docstring. This helps the AI understand the intended behavior and constraints.

Specify the programming domain: Tell the AI whether you’re testing numerical functions, string processing, API responses, or database operations. Different domains have different common edge cases.

Review generated parameters: AI might miss domain-specific edge cases particular to your business logic. Add any missing cases manually.

Test error conditions explicitly: Use pytest.raises for exception testing. Make this explicit in your prompt.

Consider performance: Very large parametrize lists slow down test runs. Prioritize the most critical edge cases.

Automating Test Generation Workflow

You can improve AI-assisted test generation with a consistent workflow:

  1. Write your function with clear type hints and docstrings

  2. Ask AI to generate parametrize test cases

  3. Run the tests to verify they pass

  4. Add missing edge cases specific to your use case

  5. Review for readability and maintainability

This workflow reduces the manual effort of identifying edge cases while ensuring human oversight of the final test suite.


Built by theluckystrike — More at zovo.one