Parametrization
The @pytest.mark.parametrize is the input variation: the decorator takes the parameter name and the list of values (or tuples), and the test runs once per value (the test_word[case0], the [case1] - the per-param item, the individual pass/fail). The parametrize replaces the loop-in-test (the one for that fails on the first bad value, the rest untested) with the per-value items (the each value its own result, the report the per-case line). The ids names the case (the readable [empty] instead of the [case0]), and the indirect applies the parametrize to the fixture (the fixture's request.param the value, the setup the per-param). This chapter covers the basic parametrize (the value, the tuple, the dict), the ids, the indirect fixture, and the parametrize on the fixture the test does not name.
The Basic Parametrize: The Value and the Tuple
# the BASIC: the the name + the the list (the
# the test RUNS once per value, the
# per-value ITEM)
import pytest
from texttools import word_count
@pytest.mark.parametrize("text,expected", [
("", 0),
("hello", 1),
("the quick brown fox", 4),
(" spaced out ", 2),
])
def test_word_count(text, expected):
assert word_count(text) == expected
# the the COLLECT: the the 4 ITEMS (the name +
# the [case0] the [case3]), the the
# the run the per-value, the report
# the per-case PASSED/FAILED
# the the ANTI-pattern (the loop the the
# the test, the first fail the
# the stop, the rest the untested):
def test_loop_anti():
cases = [("", 0), ("hello", 1), ("a b", 2)]
for text, expected in cases:
assert word_count(text) == expected
# the fail on the the ("a b") the
# ("hello") the NEVER the checked
The parametrize is the decorator: the @pytest.mark.parametrize("text,expected", [...]) takes the parameter names (the comma-separated string) and the list of tuples, and the test runs once per tuple (the test_word_count[case0] the empty, the [case1] the single, the [case3] the spaced) - the per-value item, the individual result, the report the per-case line. The anti-pattern is the loop-in-test: the for that asserts each case fails on the first bad value (the rest untested, the report the one FAILED the no which-case), and the parametrize is the fix (the each case its item, the failure names the case). The single-value form (the parametrize("text", ["", "a", "a b"])) is the one parameter, and the dict value (the {"text": ..., "expected": ...} unpacked by the ** in the test signature) is the structured case - the three forms cover the input the test varies, and the next section's ids makes the case name readable.
The ids: The Readable Case Name
# the ids: the the CASE NAME (the readable,
# the [empty] the [case0])
import pytest
from texttools import word_count
@pytest.mark.parametrize("text,expected", [
("", 0),
("hello", 1),
("the quick brown fox", 4),
], ids=["empty", "single", "four-words"])
def test_word_count(text, expected):
assert word_count(text) == expected
# the the COLLECT: the the test_word_count[empty],
# test_word_count[single], test_word_count[four-words]
# the report the READABLE (the
# the failure the [single] the case)
# the the auto-ids: the str() the value
# (the default, the
# the no ids arg the repr the)
The ids argument names the case: the ids=["empty", "single", "four-words"] replaces the [case0]/[case1] with the readable name, and the report (and the -k filter) uses the name - the failure the test_word_count[single] FAILED the case identified, not the index. The default (the no ids) is the str() of the value (the test_word_count the [] the empty repr, the a-b the value's string) - the auto-id is the value's repr, and the explicit ids is the intent (the empty the meaning, the case0 the index). The rule is the use the ids when the value is opaque (the long string, the dict, the object) and the skip it when the value is self-naming (the short string, the number) - the ids is the label, and the next section's indirect moves the parametrize from the test argument to the fixture.
The indirect: The Fixture's Param
# the INDIRECT: the PARAMETRIZE the
# FIXTURE (the request.param the
# the value, the setup the per-param)
import pytest
from texttools import word_count
@pytest.fixture
def word_text(request):
# the request.param the value
# (the setup the per-param,
# the)
text = request.param
return text.upper() if "upper" in request.node.name else text
@pytest.mark.parametrize("word_text", ["hello", "the fox"],
indirect=True)
def test_upper(word_text):
assert isinstance(word_text, str)
# the COLLECT: the 2 ITEMS (the the
# the fixture the per-param, the the
# the the test the fixture's value)
The indirect=True moves the parametrize from the test argument to the fixture: the parametrize("word_text", [...], indirect=True) sets the request.param of the word_text fixture (the fixture's setup runs per-param, the request.param the value), and the test receives the fixture's transformed value (the word_text.upper() the fixture's work, the test the assert). The use is the fixture that needs the per-param setup (the DB row the per-param id, the file the per-param content) - the indirect is the fixture's parametrize, and the direct (the no indirect) is the test argument's. The rule is the one: the value the test uses directly is the direct parametrize, the value the fixture transforms is the indirect (the request.param the fixture's input) - the two forms, and the next chapter's marks add the selection and the skip to the parametrized item.
The Working Rules
- The
parametrizeis the per-value item, not the loop. The@pytest.mark.parametrize("a,b", [...])runs the test once per tuple (the[case0]the individual result), the loop-in-test the anti-pattern (the first fail the stop, the rest untested) - the each case its item, the failure names the case. - The
idsis the readable case name. Theids=["empty", "single"]replaces the[case0](the report and the-kthe name), the default the value'sstr()repr - theidswhen the value is opaque, the skip when self-naming. - The
indirect=Trueis the fixture's parametrize. Therequest.paramthe fixture's value (the setup per-param, the transform the fixture's), the direct the test argument's - the value the fixture transforms is the indirect, the value the test uses directly is the direct. - The tuple is the multi-param, the dict the
**unpack. The("text,expected", [(.., ..)])the two parameters, the({"text": .., "expected": ..},)the**casein the signature - the structured case the dict, the positional the tuple.
Summary
The @pytest.mark.parametrize is the input variation: the decorator takes the parameter name and the list of values/tuples, and the test runs once per value (the test_word[case0] the per-value item, the individual pass/fail, the report the per-case line) - the fix for the loop-in-test (the first fail the stop, the rest untested). The ids argument names the case (the [empty] the readable, the default the value's str() repr), and the indirect=True applies the parametrize to the fixture (the request.param the fixture's value, the setup per-param, the transform the fixture's) while the direct is the test argument's. The forms are the single-value, the tuple (the multi-param), and the dict (the ** unpack), and the rule is the split: the value the test uses directly is the direct parametrize, the value the fixture transforms is the indirect. The next chapter's marks add the selection (the -m), the skip, and the expected-fail to the parametrized item - the parametrize the what, the mark the how-selected.