Parametrized tests

The problem: near-identical tests, differing only in data

Testing several input/output cases for the same behavior tends to produce nearly-duplicated test functions:

def test_valid_name_search():
    assert is_valid_tool_name("search") == True

def test_valid_name_calculator():
    assert is_valid_tool_name("calculator") == True

def test_invalid_name_empty():
    assert is_valid_tool_name("") == False

def test_invalid_name_number():
    assert is_valid_tool_name(42) == False

Four functions, each just running the same single line against a different input — exactly the kind of repetition this course has pushed back on since comprehensions, just showing up in test code instead of application code.

pytest -v
click Run to see this pane's output

Four entirely separate functions in the report, one per case — every one of them exercising the exact same single line of logic, just with different inputs typed out by hand each time.

@pytest.mark.parametrize — one test, many cases

import pytest

@pytest.mark.parametrize("name, expected", [
    ("search", True),
    ("calculator", True),
    ("", False),
    (42, False),
])
def test_is_valid_tool_name(name, expected):
    assert is_valid_tool_name(name) == expected

"name, expected" names the parameters the test function will receive; the list of tuples supplies one set of values per test case — pytest runs test_is_valid_tool_name once per tuple, reporting each one as its own separate pass or fail:

pytest -v
click Run to see this pane's output

One test function, four actual test runs, each individually reported — adding a fifth case means adding one line to the list, not writing an entire new function.

Parametrizing a TestClient-based test

The same mechanism applies directly to an integration test using TestClient:

@pytest.mark.parametrize("payload, expected_status", [
    ({"name": "research_agent", "model": "claude-sonnet"}, 201),
    ({"name": "research_agent"}, 422),               # missing required "model"
    ({"model": "claude-sonnet"}, 422),                # missing required "name"
    ({"name": 123, "model": "claude-sonnet"}, 422),   # wrong type for "name"
])
def test_create_agent_validation(client, payload, expected_status):
    response = client.post("/agents", json=payload, headers={"X-Api-Key": "secret-key-123"})
    assert response.status_code == expected_status
pytest
click Run to see this pane's output

Notice client (a fixture) and payload/expected_status (parametrized values) both appear as parameters on the same test function at once — pytest handles injecting both kinds without any conflict between them.

A note on this site’s own exercise below: everywhere else in this course, a real synchronous TestClient (shown above, and in Concepts 1–3) is exactly what you’d actually write. The graded exercise on this page runs your test for real, in-browser, at zero cost — which specifically rules out a real background thread, the thing a synchronous TestClient needs internally. So just for that exercise, write async def and await client.post(...) instead — same @pytest.mark.parametrize, same fixture, same real assertions, actually executed and checked; only the two keywords differ, for the same reason this course’s FastAPI routes needed them too.

Check your understanding
1/3

What problem does @pytest.mark.parametrize solve?

Exercise · Graded (real pytest, in-browser)

Given the client fixture and the agent registry's POST /agents endpoint, write a single parametrized async test function named test_create_agent_cases covering: a valid payload (expect 201), a payload missing "name" (expect 422), a payload missing "model" (expect 422), a payload with "name" as an integer instead of a string (expect 422), and the same valid payload sent with no X-Api-Key header at all (expect 401). Use @pytest.mark.parametrize with three parameters: payload, include_api_key, and expected_status.