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) == FalseFour 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.
click Run to see this pane's outputFour 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:
click Run to see this pane's outputOne 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_statusclick Run to see this pane's outputNotice 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.
What problem does @pytest.mark.parametrize solve?
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.