parametrize — एक टेस्ट, कई केस
कॉपी किए गए टेस्ट और टेस्ट के अंदर लूप की जगह @pytest.mark.parametrize। टेस्ट ID, ids= और pytest.param, decorator स्टैक करना, एक ही टेबल में वैल्यू और एरर, और आम गलतियाँ।
- 1समस्या
- 2समझें
- 3उदाहरण
- 4अनुमान
- 5स्वयं करें
- 6चुनौती
वह समस्या जिसे हम हल कर रहे हैं
यह एक छोटा फ़ंक्शन है जो परीक्षा के अंकों को ग्रेड में बदलता है, grading.py:
def grade(score):
if not 0 <= score <= 100:
raise ValueError(f"score must be between 0 and 100, got {score}")
if score >= 80:
return "A"
if score >= 60:
return "B"
if score >= 40:
return "C"
return "F"$ python3 -c "from grading import grade; print(grade(95), grade(79), grade(40), grade(0))"
A B C Fइसे ठीक से टेस्ट करने का मतलब है कई अंकों को जाँचना, ख़ासकर किनारों वाले अंकों को: 80 और 79, 60 और 59, 40 और 39। इसे लिखने का पहला तरीक़ा जो हर किसी को सूझता है, वह है एक टेस्ट कॉपी करना और दो वैल्यूज़ बदल देना:
from grading import grade
def test_grade_95():
assert grade(95) == "A"
def test_grade_79():
assert grade(79) == "B"
def test_grade_40():
assert grade(40) == "C"$ pytest -q test_copy.py
... [100%]
3 passed in 0.01sयह काम करता है, पर बढ़ता नहीं। तीन फ़ंक्शन एक ही बात कहते हैं — "यह अंक यह ग्रेड देता है" — और उनमें सिर्फ़ दो वैल्यूज़ का फ़र्क़ है। दस और केस का मतलब है दस और कॉपियाँ, जिन्हें आपस में मेल में रखना होगा।
तो अगला विचार है: केसों की एक सूची, और एक ही टेस्ट के अंदर एक लूप। नीचे दो अपेक्षित ग्रेड जानबूझकर ग़लत हैं — 79 का B होना चाहिए, और 40 का C:
from grading import grade
def test_grades():
cases = [(95, "A"), (80, "A"), (79, "C"), (40, "D"), (0, "F")]
for score, expected in cases:
assert grade(score) == expected$ pytest -q --tb=short test_loop.py
F [100%]
=================================== FAILURES ===================================
_________________________________ test_grades __________________________________
test_loop.py:7: in test_grades
assert grade(score) == expected
E AssertionError: assert 'B' == 'C'
E
E - C
E + B
=========================== short test summary info ============================
FAILED test_loop.py::test_grades - AssertionError: assert 'B' == 'C'
1 failed in 0.01sयह रिपोर्ट जितनी दिखती है उससे बुरी है। ग़लत केस दो थे, और ख़बर आपको एक की मिली: पहला फ़ेल होने वाला assert टेस्ट को वहीं ख़त्म कर देता है, इसलिए 40 वाला केस कभी चला ही नहीं। रिपोर्ट यह भी नहीं बताती कि कौन-सा अंक फ़ेल हुआ — आपको ख़ुद अंदाज़ा लगाना पड़ता है कि 'B' == 'C' 79 से आया। और "1 failed" पाँच केसों को एक ही तथ्य गिनता है। पहला ठीक करें, फिर चलाएँ, और तभी दूसरे से मुलाक़ात होगी।
आपको चाहिए लूप जितनी संक्षिप्तता और कॉपियों जैसी रिपोर्टिंग: एक फ़ंक्शन, कई अलग-अलग टेस्ट।
इस अध्याय के अंत में आप कर पाएंगे
@pytest.mark.parametrizeसे एक टेस्ट फ़ंक्शन को कई केसों पर चलाना- हर केस में कई वैल्यूज़ देना और pytest के बनाए टेस्ट ID पढ़ना
ids=औरpytest.param(..., id=...)से केसों को नाम देना- बाक़ी केसों को छुए बिना किसी एक केस को
xfailयाskipमार्क करना - दो parametrize डेकोरेटर जोड़कर उनकी वैल्यूज़ की हर जोड़ी चलाना
- "वैल्यू लौटाता है" और "एरर उठाता है" वाले केसों को एक ही टेबल में रखना
- parametrize की आम ग़लतियों को उनके एरर मैसेज से पहचानना
ज़रूरी शर्तें: `pytest.approx` से float की तुलना।
टेस्ट लिखने से पहले
Parametrize असल में एक टेबल लिखने का तरीक़ा है। इसलिए किसी भी टेस्ट कोड से पहले का काम है वह टेबल बनाना, और यह तय करना कि टेबल सही रूप है भी या नहीं।
1. कॉन्ट्रैक्ट लिखें। grade(score) वादा करता है: 0 से 100 तक का एक पूर्णांक अंक अंदर जाता है; "A", "B", "C", "F" में से एक बाहर आता है; 0–100 के बाहर की कोई भी चीज़ ValueError उठाती है, जिसके मैसेज में वह अंक लिखा होता है। इसका कोई side effect नहीं है — न फ़ाइल, न प्रिंट — इसलिए न कुछ सेटअप करना है, न साफ़ करना। यही इसे उपयुक्त बनाता है: हर केस बस "ये इनपुट, वह नतीजा" है।
2. अपना सेटअप जाँचें। आपको सक्रिय virtual environment, इंस्टॉल किया हुआ pytest, और टेस्ट फ़ाइल से import हो सकने वाला grading.py चाहिए — इन उदाहरणों में वह टेस्टों के साथ उसी फ़ोल्डर में है। और कुछ नहीं: parametrize pytest में ही बना है, और contextlib स्टैंडर्ड लाइब्रेरी में है।
3. केसों की योजना बनाएँ। सबसे ज़्यादा मायने सीमाओं के हैं, क्योंकि >= बनाम > की ग़लतियाँ वहीं रहती हैं। हर नियम को उसकी रेखा के दोनों ओर की वैल्यू मिलती है:
| केस | इनपुट | अपेक्षित | |---|---|---| | रेंज का सबसे ऊपरी सिरा | 100 | "A" | | सबसे कम A | 80 | "A" | | सबसे ऊँचा B | 79 | "B" | | सबसे कम B | 60 | "B" | | सबसे ऊँचा C | 59 | "C" | | सबसे कम C | 40 | "C" | | सबसे ऊँचा F | 39 | "F" | | रेंज का सबसे निचला सिरा | 0 | "F" | | रेंज से ठीक नीचे | -1 | ValueError | | रेंज से ठीक ऊपर | 101 | ValueError |
हर पंक्ति एक केस बनती है। "केस" वाला कॉलम सजावट नहीं है — आगे चलकर वही टेस्ट ID बनता है, ताकि फ़ेल होने पर पता चले कि कौन-सा नियम टूटा।
4. तय करें कि क्या टेस्ट नहीं करना है। 0 से 100 तक हर अंक नहीं: 95 और 85 वही नियम जाँचते हैं जो 80, और बिना नई जानकारी जोड़े बस समय बढ़ाते हैं। हर सीमा के हर तरफ़ एक वैल्यू काफ़ी है। Python के अपने comparison operators को टेस्ट न करें, और दो अलग व्यवहारों को सिर्फ़ इसलिए एक टेस्ट में न डालें कि वे एक टेबल में समा जाते हैं — अगर पंक्तियों को अलग-अलग assertion चाहिए, तो वे अलग टेस्टों की हैं।
बाक़ी अध्याय इसी टेबल को एक-एक औज़ार से कोड में बदलता है।
एक आर्ग्युमेंट, कई वैल्यूज़
डेकोरेटर दो चीज़ें लेता है: एक आर्ग्युमेंट का नाम और उसके लिए वैल्यूज़ की सूची। Pytest हर वैल्यू के लिए एक टेस्ट बनाता है और उस वैल्यू को उसी नाम से अंदर भेजता है:
import pytest
from grading import grade
@pytest.mark.parametrize("score", [80, 95, 100])
def test_top_grade(score):
assert grade(score) == "A"$ pytest -v test_one.py
collecting ... collected 3 items
test_one.py::test_top_grade[80] PASSED [ 33%]
test_one.py::test_top_grade[95] PASSED [ 66%]
test_one.py::test_top_grade[100] PASSED [100%]
============================== 3 passed in 0.01s ===============================एक फ़ंक्शन, तीन टेस्ट, हर एक की अपनी लाइन और अपना नाम। वर्गाकार कोष्ठकों वाला हिस्सा टेस्ट ID है, जो वैल्यू से बना है। score कोई fixture नहीं है और उसकी कोई default वैल्यू नहीं — डेकोरेटर स्ट्रिंग का नाम बस फ़ंक्शन के किसी पैरामीटर से मेल खाना चाहिए।
हर केस में कई आर्ग्युमेंट
ज़्यादातर केसों को एक इनपुट और एक अपेक्षित नतीजा चाहिए। दोनों के नाम कॉमा से अलग करके लिखें, और हर केस को उसी क्रम में एक tuple के रूप में दें। यह रहा वही लूप फिर से, वही दो ग़लत अपेक्षाओं के साथ:
import pytest
from grading import grade
@pytest.mark.parametrize(
"score,expected",
[(95, "A"), (80, "A"), (79, "C"), (40, "D"), (0, "F")],
)
def test_grade(score, expected):
assert grade(score) == expected$ pytest -q --tb=short test_param.py
..FF. [100%]
=================================== FAILURES ===================================
_______________________________ test_grade[79-C] _______________________________
test_param.py:11: in test_grade
assert grade(score) == expected
E AssertionError: assert 'B' == 'C'
E
E - C
E + B
_______________________________ test_grade[40-D] _______________________________
test_param.py:11: in test_grade
assert grade(score) == expected
E AssertionError: assert 'C' == 'D'
E
E - D
E + C
=========================== short test summary info ============================
FAILED test_param.py::test_grade[79-C] - AssertionError: assert 'B' == 'C'
FAILED test_param.py::test_grade[40-D] - AssertionError: assert 'C' == 'D'
2 failed, 3 passed in 0.01sइसकी तुलना लूप से करें। हर केस चला। दोनों फ़ेल रिपोर्ट हुए, हर एक ऐसे ID के नीचे — test_grade[79-C] — जो केस का नाम बताता है। गिनती ईमानदार है: 2 failed, 3 passed। (डिफ़ॉल्ट लंबे traceback में pytest हर फ़ेल केस के आर्ग्युमेंट भी छापता है, score = 79, expected = 'C'।)
नाम एक स्ट्रिंग के रूप में लिखे जा सकते हैं, "score,expected" या "score, expected" — कॉमा के बाद स्पेस चलता है — या स्ट्रिंग्स की सूची के रूप में, ["score", "expected"]। दोनों बिलकुल एक जैसे टेस्ट बनाते हैं। स्ट्रिंग आम रूप है; सूची तब काम आती है जब नाम कोड से बनाए जा रहे हों।
ID कैसे बनते हैं
संख्याओं, स्ट्रिंग्स, booleans और None के लिए pytest वैल्यू को ही ID में रख देता है और एक केस की वैल्यूज़ को - से जोड़ता है। बाक़ी किसी भी चीज़ के लिए — list, dict, object — वह कोशिश ही नहीं करता। वह आर्ग्युमेंट का नाम और केस की स्थिति इस्तेमाल करता है:
import pytest
@pytest.mark.parametrize(
["value", "expected"],
[
(1.5, True),
("hello world", None),
([1, 2], {"a": 1}),
],
)
def test_show_ids(value, expected):
pass$ pytest -v test_ids_auto.py
collecting ... collected 3 items
test_ids_auto.py::test_show_ids[1.5-True] PASSED [ 33%]
test_ids_auto.py::test_show_ids[hello world-None] PASSED [ 66%]
test_ids_auto.py::test_show_ids[value2-expected2] PASSED [100%]
============================== 3 passed in 0.01s ===============================value2-expected2 का मतलब है "केस नंबर 2 का value और expected, शून्य से गिनते हुए"। यह अनोखा है, और कुछ नहीं बताता। ऐसा केस फ़ेल हो तो उसे ढूँढने के लिए आपको सूची में नीचे गिनना पड़ता है। यही वजह है कि केसों को नाम ख़ुद दें।
केसों को नाम देना: ids=
ids= को स्ट्रिंग्स की एक सूची दें, हर केस के लिए एक, उसी क्रम में:
import pytest
from grading import grade
@pytest.mark.parametrize(
"score,expected",
[(80, "A"), (79, "B"), (40, "C"), (39, "F")],
ids=["lowest-A", "highest-B", "lowest-C", "highest-F"],
)
def test_grade_boundaries(score, expected):
assert grade(score) == expected$ pytest -v test_ids_list.py
collecting ... collected 4 items
test_ids_list.py::test_grade_boundaries[lowest-A] PASSED [ 25%]
test_ids_list.py::test_grade_boundaries[highest-B] PASSED [ 50%]
test_ids_list.py::test_grade_boundaries[lowest-C] PASSED [ 75%]
test_ids_list.py::test_grade_boundaries[highest-F] PASSED [100%]
============================== 4 passed in 0.01s ===============================अब आउटपुट बताता है कि हर केस क्यों है। कमज़ोरी यह है कि नाम एक दूसरी सूची में रहते हैं, अपने केसों से दूर: बीच में एक केस जोड़ें, उसका नाम भूल जाएँ, और उसके बाद का हर नाम एक जगह खिसक जाएगा।
ids= एक फ़ंक्शन भी लेता है। Pytest उसे हर वैल्यू के लिए एक बार बुलाता है, हर केस के लिए नहीं, और नतीजों को - से जोड़ता है:
import pytest
from grading import grade
def describe(value):
if isinstance(value, int):
return f"score={value}"
return f"grade={value}"
@pytest.mark.parametrize("score,expected", [(80, "A"), (79, "B")], ids=describe)
def test_grade_named(score, expected):
assert grade(score) == expected$ pytest -v test_ids_func.py
collecting ... collected 2 items
test_ids_func.py::test_grade_named[score=80-grade=A] PASSED [ 50%]
test_ids_func.py::test_grade_named[score=79-grade=B] PASSED [100%]
============================== 2 passed in 0.01s ===============================फ़ंक्शन उन वैल्यूज़ के लिए ठीक है जिनका अपने-आप बना ID बेकार है, जैसे objects। अगर वह किसी वैल्यू के लिए None लौटाए, तो pytest उस वैल्यू के लिए अपने-आप बने ID पर लौट आता है।
एक बार में एक केस: pytest.param
pytest.param एक अकेले केस को लपेटता है और उसमें चीज़ें जोड़ने देता है: एक id= जो उसकी वैल्यूज़ के ठीक बगल में रहता है, और marks= जो सिर्फ़ उसी केस पर लागू होते हैं।
import pytest
from grading import grade
@pytest.mark.parametrize(
"score,expected",
[
pytest.param(100, "A", id="perfect"),
pytest.param(60, "B", id="lowest-B"),
pytest.param(59.5, "B", id="round-up", marks=pytest.mark.xfail(reason="no rounding yet")),
pytest.param(0, "F", id="zero", marks=pytest.mark.skip(reason="example of skip")),
],
)
def test_grade_cases(score, expected):
assert grade(score) == expected$ pytest -v test_param_obj.py
collecting ... collected 4 items
test_param_obj.py::test_grade_cases[perfect] PASSED [ 25%]
test_param_obj.py::test_grade_cases[lowest-B] PASSED [ 50%]
test_param_obj.py::test_grade_cases[round-up] XFAIL (no rounding yet) [ 75%]
test_param_obj.py::test_grade_cases[zero] SKIPPED (example of skip) [100%]
=================== 2 passed, 1 skipped, 1 xfailed in 0.01s ====================round-up केस एक ऐसा फ़ैसला दर्ज करता है जो अभी लिया नहीं गया — क्या 59.5 ऊपर होकर B बनना चाहिए? — और वह भी रन को फ़ेल किए बिना; बाक़ी केस अछूते रहते हैं। (xfail और skip का आगे अपना अध्याय है; अभी इतना जानना काफ़ी है कि एक मार्क एक केस पर लग सकता है।)
चूँकि ID एक असली नाम है, एक केस को अकेले चलाया जा सकता है: pytest "test_param_obj.py::test_grade_cases[perfect]"। इसे quotes में रखें — शेल वर्गाकार कोष्ठकों को ख़ास तरीक़े से समझता है।
दो डेकोरेटर: हर संयोजन
दो parametrize डेकोरेटर एक के ऊपर एक रखें और pytest उनकी वैल्यूज़ की हर जोड़ी के लिए टेस्ट चलाता है — cartesian product। दो मुद्राएँ गुणा तीन रक़में, यानी छह टेस्ट:
import pytest
@pytest.mark.parametrize("currency", ["USD", "EUR"])
@pytest.mark.parametrize("amount", [0, 10, 999])
def test_format(amount, currency):
text = f"{amount} {currency}"
assert text.endswith(currency)$ pytest -v test_stack.py
collecting ... collected 6 items
test_stack.py::test_format[0-USD] PASSED [ 16%]
test_stack.py::test_format[0-EUR] PASSED [ 33%]
test_stack.py::test_format[10-USD] PASSED [ 50%]
test_stack.py::test_format[10-EUR] PASSED [ 66%]
test_stack.py::test_format[999-USD] PASSED [ 83%]
test_stack.py::test_format[999-EUR] PASSED [100%]
============================== 6 passed in 0.01s ===============================फ़ंक्शन के सबसे पास वाला डेकोरेटर ID का पहला हिस्सा देता है। स्टैक तभी करें जब इनपुट सच में स्वतंत्र हों — हर रक़म हर मुद्रा के साथ चलनी चाहिए। अगर सिर्फ़ कुछ जोड़ियाँ ही मायने रखती हैं, तो उन्हें एक डेकोरेटर में tuples के रूप में लिखें। और गुणा पर नज़र रखें: दस-दस वैल्यूज़ वाले तीन डेकोरेटर हज़ार टेस्ट बनाते हैं।
वैल्यूज़ और एरर एक ही टेबल में
कभी-कभी केस दो हिस्सों में बँट जाते हैं: "यह लौटाता है" और "वह उठाता है"। आप अपेक्षा को ही एक पैरामीटर बना सकते हैं, और with से उसमें प्रवेश कर सकते हैं:
from contextlib import nullcontext
import pytest
from grading import grade
@pytest.mark.parametrize(
"score,expectation",
[
(50, nullcontext("C")),
(100, nullcontext("A")),
(-1, pytest.raises(ValueError)),
(101, pytest.raises(ValueError, match="got 101")),
],
)
def test_grade_or_error(score, expectation):
with expectation as expected:
assert grade(score) == expected$ pytest -v test_raises.py
collecting ... collected 4 items
test_raises.py::test_grade_or_error[50-expectation0] PASSED [ 25%]
test_raises.py::test_grade_or_error[100-expectation1] PASSED [ 50%]
test_raises.py::test_grade_or_error[-1-expectation2] PASSED [ 75%]
test_raises.py::test_grade_or_error[101-expectation3] PASSED [100%]
============================== 4 passed in 0.01s ===============================contextlib.nullcontext एक context manager है जो कुछ नहीं करता: with nullcontext("C") as expected बस expected = "C" सेट करता है, इसलिए सामान्य केस assert को हमेशा की तरह चलाते हैं। एरर वाले केसों में pytest.raises context manager है: grade उसके अंदर एरर उठाता है, assert कभी पूरा नहीं होता, और टेस्ट पास होता है क्योंकि सही एरर आया। अगर कोई एरर न आए, तो आपको जाना-पहचाना DID NOT RAISE मिलता है।
छोटी टेबल के लिए यह साफ़-सुथरा है। अगर टेस्ट के अंदर दोनों तरह के केसों को अलग पहचानने के लिए if लिखने पड़ें, तो उसे दो टेस्टों में बाँट दें। ID पर भी ध्यान दें: expectation0 फिर से वही object वाला नियम है। पूरा उदाहरण इसे pytest.param से ठीक करता है।
एक क्लास को parametrize करना
डेकोरेटर को एक टेस्ट क्लास पर लगाएँ और उसके अंदर का हर टेस्ट मेथड वह पैरामीटर पाता है:
import pytest
from grading import grade
@pytest.mark.parametrize("score", [80, 100])
class TestTopBand:
def test_is_a(self, score):
assert grade(score) == "A"
def test_is_a_string(self, score):
assert isinstance(grade(score), str)$ pytest -v test_class.py
collecting ... collected 4 items
test_class.py::TestTopBand::test_is_a[80] PASSED [ 25%]
test_class.py::TestTopBand::test_is_a[100] PASSED [ 50%]
test_class.py::TestTopBand::test_is_a_string[80] PASSED [ 75%]
test_class.py::TestTopBand::test_is_a_string[100] PASSED [100%]
============================== 4 passed in 0.01s ===============================दो मेथड गुणा दो वैल्यूज़: चार टेस्ट। क्लास का हर मेथड score स्वीकार करे, वरना collection फ़ेल होगा।
पूर्ण उदाहरण
shipping.py:
import math
RATES = {"local": 50, "national": 120}
def shipping_cost(weight_kg, zone):
if zone not in RATES:
raise KeyError(f"unknown zone: {zone}")
if weight_kg <= 0:
raise ValueError("weight must be positive")
# The first kilogram is in the base rate; each started kilogram after it costs 20.
return RATES[zone] + (math.ceil(weight_kg) - 1) * 20$ python3 -c "from shipping import shipping_cost; print(shipping_cost(0.5, 'local'), shipping_cost(1.2, 'local'), shipping_cost(3, 'national'))"
50 70 160test_shipping.py:
from contextlib import nullcontext
import pytest
from shipping import shipping_cost
@pytest.mark.parametrize(
"weight_kg,zone,expected",
[
pytest.param(0.5, "local", 50, id="light-local"),
pytest.param(1, "local", 50, id="exactly-1kg"),
pytest.param(1.2, "local", 70, id="part-kg-rounds-up"),
pytest.param(3, "national", 160, id="heavy-national"),
],
)
def test_cost(weight_kg, zone, expected):
assert shipping_cost(weight_kg, zone) == expected
@pytest.mark.parametrize("zone", ["local", "national"])
@pytest.mark.parametrize("weight_kg", [0.1, 0.5, 1])
def test_first_kg_is_flat(weight_kg, zone):
assert shipping_cost(weight_kg, zone) == shipping_cost(1, zone)
@pytest.mark.parametrize(
"weight_kg,zone,expectation",
[
pytest.param(2, "local", nullcontext(70), id="ok"),
pytest.param(0, "local", pytest.raises(ValueError), id="zero-weight"),
pytest.param(-1, "local", pytest.raises(ValueError), id="negative"),
pytest.param(1, "moon", pytest.raises(KeyError, match="moon"), id="bad-zone"),
],
)
def test_cost_or_error(weight_kg, zone, expectation):
with expectation as expected:
assert shipping_cost(weight_kg, zone) == expected$ pytest -v test_shipping.py
collecting ... collected 14 items
test_shipping.py::test_cost[light-local] PASSED [ 7%]
test_shipping.py::test_cost[exactly-1kg] PASSED [ 14%]
test_shipping.py::test_cost[part-kg-rounds-up] PASSED [ 21%]
test_shipping.py::test_cost[heavy-national] PASSED [ 28%]
test_shipping.py::test_first_kg_is_flat[0.1-local] PASSED [ 35%]
test_shipping.py::test_first_kg_is_flat[0.1-national] PASSED [ 42%]
test_shipping.py::test_first_kg_is_flat[0.5-local] PASSED [ 50%]
test_shipping.py::test_first_kg_is_flat[0.5-national] PASSED [ 57%]
test_shipping.py::test_first_kg_is_flat[1-local] PASSED [ 64%]
test_shipping.py::test_first_kg_is_flat[1-national] PASSED [ 71%]
test_shipping.py::test_cost_or_error[ok] PASSED [ 78%]
test_shipping.py::test_cost_or_error[zero-weight] PASSED [ 85%]
test_shipping.py::test_cost_or_error[negative] PASSED [ 92%]
test_shipping.py::test_cost_or_error[bad-zone] PASSED [100%]
============================== 14 passed in 0.01s ==============================तीन फ़ंक्शन, चौदह टेस्ट, और हर लाइन बताती है कि उसने क्या जाँचा। तीन बातें ध्यान देने लायक़ हैं।
पहली, test_cost के ID हर केस की वजह बताते हैं — part-kg-rounds-up उस नियम का नाम है जिसकी रक्षा हो रही है, जो 1.2-local-70 नहीं बताता।
दूसरी, test_first_kg_is_flat स्टैक करता है क्योंकि वज़न और ज़ोन सच में स्वतंत्र हैं। वह फ़ंक्शन की तुलना किसी संख्या से नहीं बल्कि ख़ुद से करता है, इसलिए दरें बदलने पर भी टिका रहता है।
तीसरी, pytest.param(..., id=...) expectation2 को negative बना देता है। वैल्यूज़ और एरर की टेबल अब एक specification की तरह पढ़ी जाती है।
जब यह काम न करे
function uses no argument 'scores' डेकोरेटर स्ट्रिंग का कोई नाम फ़ंक्शन के किसी पैरामीटर से मेल नहीं खाता — यहाँ "scores,expected" बनाम def test_grade(score, expected)। यह collection के समय फ़ेल होता है, कोई टेस्ट चलने से पहले:
import pytest
from grading import grade
@pytest.mark.parametrize("scores,expected", [(80, "A"), (40, "C")])
def test_grade(score, expected):
assert grade(score) == expected$ pytest -q test_mismatch.py
==================================== ERRORS ====================================
______________________ ERROR collecting test_mismatch.py _______________________
In test_mismatch.py::test_grade: function uses no argument 'scores'
=========================== short test summary info ============================
ERROR test_mismatch.py - Failed: In test_mismatch.py::test_grade: function us...
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.01sfixture 'expected' not found उलटी ग़लती: फ़ंक्शन में एक ऐसा पैरामीटर है जिसका नाम डेकोरेटर में कभी नहीं आता, इसलिए pytest उस नाम का fixture ढूँढने जाता है और कुछ नहीं पाता। नाम को डेकोरेटर स्ट्रिंग में जोड़ें।
the number of names (2) ... must be equal to the number of values (1) किसी एक केस में वैल्यूज़ की संख्या ग़लत है — मान लीजिए "score,expected" वाली सूची में (40,):
$ pytest -q test_count.py
==================================== ERRORS ====================================
________________________ ERROR collecting test_count.py ________________________
test_count.py::test_grade: in "parametrize" the number of names (2):
['score', 'expected']
must be equal to the number of values (1):
(40,)[score0] जैसे ID और tuple का ज़िक्र करता TypeError एक आर्ग्युमेंट नाम के साथ, सूची की हर वैल्यू पूरी की पूरी भेजी जाती है। आदतन वहाँ tuples लिख दें तो फ़ंक्शन को tuple मिलता है:
import pytest
from grading import grade
@pytest.mark.parametrize("score", [(80,), (95,)])
def test_top_grade(score):
assert grade(score) == "A"$ pytest -v --tb=no test_tuples.py
collecting ... collected 2 items
test_tuples.py::test_top_grade[score0] FAILED [ 50%]
test_tuples.py::test_top_grade[score1] FAILED [100%]
=========================== short test summary info ============================
FAILED test_tuples.py::test_top_grade[score0] - TypeError: '<=' not supported...
FAILED test_tuples.py::test_top_grade[score1] - TypeError: '<=' not supported...
============================== 2 failed in 0.01s ===============================ID ही सुराग़ है: सादी संख्या होती तो [80] मिलता, इसलिए [score0] बताता है कि वैल्यू कुछ और थी। एक आर्ग्युमेंट के लिए [80, 95] लिखें।
एक केस अकेले पास होता है और बाक़ियों के साथ फ़ेल पैरामीटर वैल्यूज़ एक बार बनती हैं, जब मॉड्यूल collect होता है, और वही object हर उस टेस्ट को दिया जाता है जो उसे पाता है। अगर कोई टेस्ट एक mutable वैल्यू — list, dict — को बदल दे, तो अगला टेस्ट वह बदलाव देखता है:
import pytest
@pytest.mark.parametrize("cart", [[]])
class TestCart:
def test_add_item(self, cart):
cart.append("pen")
assert cart == ["pen"]
def test_starts_empty(self, cart):
assert cart == []$ pytest -q --tb=short test_shared.py
.F [100%]
=================================== FAILURES ===================================
______________________ TestCart.test_starts_empty[cart0] _______________________
test_shared.py:11: in test_starts_empty
assert cart == []
E AssertionError: assert ['pen'] == []
E
E Left contains one more item: 'pen'
E Use -v to get more diff
=========================== short test summary info ============================
FAILED test_shared.py::TestCart::test_starts_empty[cart0] - AssertionError: a...
1 failed, 1 passed in 0.01sकोई immutable चीज़ दें, जैसे tuple (), और हर टेस्ट के अंदर cart = list(items) से नया object बनाएँ। तब हर टेस्ट अपनी ख़ाली सूची से शुरू होता है और दोनों पास होते हैं।
चरण 4 / 6 — अनुमान
अपनी समझ की जाँच करें
pytest -v चलाने पर कौन-से चार टेस्ट ID, किस क्रम में दिखेंगे?
import pytest
@pytest.mark.parametrize("b", ["x", "y"])
@pytest.mark.parametrize("a", [1, 2])
def test_pair(a, b):
pass- Atest_pair[x-1], test_pair[x-2], test_pair[y-1], test_pair[y-2]
- Btest_pair[x-1], test_pair[y-1], test_pair[x-2], test_pair[y-2]
- Ctest_pair[1-x], test_pair[1-y], test_pair[2-x], test_pair[2-y]
- Dtest_pair[1-x], test_pair[2-y]
grade(80) और grade(95) दोनों "A" लौटाते हैं, फिर भी दोनों टेस्ट फ़ेल होते हैं। क्यों?
import pytest
from grading import grade
@pytest.mark.parametrize("score", [(80,), (95,)])
def test_top_grade(score):
assert grade(score) == "A"- Aसिर्फ़ एक argument नाम के साथ parametrize इस्तेमाल नहीं हो सकता
- Bनाम एक हो तो हर वैल्यू पूरी की पूरी जाती है, इसलिए `score` tuple `(80,)` है, `80` नहीं
- C`score0` और `score1` ID फ़ंक्शन के नाम से टकराते हैं
- Dवैल्यूज़ list नहीं, tuple के tuple होनी चाहिए
यहाँ दो अपेक्षाएँ गलत हैं (79 का B होना चाहिए, 40 का C)। pytest क्या रिपोर्ट करेगा?
def test_grades():
cases = [(95, "A"), (79, "C"), (40, "D")]
for score, expected in cases:
assert grade(score) == expected- A2 failed, 1 passed — हर केस के लिए एक लाइन
- B3 failed, क्योंकि एक फ़ेल होने से हर केस फ़ेल हो जाता है
- C1 failed — 40 वाले केस के लिए, जो आख़िरी गलत केस है
- D1 failed — सिर्फ़ पहले गलत केस के लिए; 40 वाला केस कभी चलता ही नहीं
उत्तर देने के लिए अकाउंट आवश्यक है
अपने उत्तर जाँचने के लिए साइन इन करें
प्रश्न ऊपर दिए गए हैं, और मन में उत्तर सोचना ही मुख्य कार्य है। सही उत्तर, व्याख्या और तीन-स्तरीय संकेत देखने के लिए साइन इन करें।
आपकी बारी
password.py लिखें जिसमें एक फ़ंक्शन check_password(text) हो जो समस्याओं की सूची लौटाए: 8 अक्षरों से कम हो तो "too short", कोई अंक न हो तो "no digit", कोई अक्षर न हो तो "no letter"। अच्छा पासवर्ड [] लौटाता है। जो कुछ भी str नहीं है, वह TypeError उठाता है।
फिर test_password.py लिखें जिसमें हो:
- कम से कम छह केसों वाला एक parametrized टेस्ट,
pytest.paramसे लिखा हुआ, और ऐसेid=के साथ जो बताए कि हर केस किस बारे में है ("seven-chars","digits-only"वग़ैरह) - दो स्टैक किए गए डेकोरेटर — तीन अच्छे पासवर्ड गुणा दो suffix,
""और"!"— जो जाँचें कि सभी छह[]लौटाते हैं - एक टेबल जो सामान्य केसों (
nullcontext) कोTypeErrorकेसों (pytest.raises) के साथ मिलाए — दूसरी तरह के लिएNoneऔर integer12345678अच्छे इनपुट हैं - एक केस जो
xfailमार्क हो, किसी ऐसे नियम के लिए जो आपने अभी नहीं लिखा, जैसे "no uppercase letter"
pytest -v चलाएँ और ID की सूची ऐसे पढ़ें जैसे किसी और ने लिखी हो। क्या आप सिर्फ़ ID से पासवर्ड के नियम बता सकते हैं? अगर नहीं, तो केसों के नाम तब तक बदलें जब तक बता न सकें।
फिर जानबूझकर एक अपेक्षा तोड़ें और दोबारा चलाएँ। जाँचें कि ठीक एक लाइन फ़ेल होती है, उसका ID बताता है कि वह कौन-सा केस है, और बाक़ी हर केस फिर भी चला।
समाधान
पहले कॉन्ट्रैक्ट, फिर टेबल। check_password एक str लेता है और मिली हर समस्या की सूची एक तय क्रम में लौटाता है; non-str कुछ लौटाने के बजाय TypeError उठाता है। password.py:
def check_password(text):
if not isinstance(text, str):
raise TypeError(f"password must be a str, got {type(text).__name__}")
problems = []
if len(text) < 8:
problems.append("too short")
if not any(ch.isdigit() for ch in text):
problems.append("no digit")
if not any(ch.isalpha() for ch in text):
problems.append("no letter")
return problems$ python3 -c "from password import check_password as c; print(c('abcdef12'), c('ab1'), c(''))"
[] ['too short'] ['too short', 'no digit', 'no letter']test_password.py:
from contextlib import nullcontext
import pytest
from password import check_password
@pytest.mark.parametrize(
"text,expected",
[
pytest.param("abcdef12", [], id="eight-chars-ok"),
pytest.param("abcde12", ["too short"], id="seven-chars"),
pytest.param("abcdefgh", ["no digit"], id="letters-only"),
pytest.param("12345678", ["no letter"], id="digits-only"),
pytest.param("", ["too short", "no digit", "no letter"], id="empty"),
pytest.param("ab1", ["too short"], id="short-but-mixed"),
pytest.param(
"abcdef12",
["no uppercase"],
id="needs-uppercase",
marks=pytest.mark.xfail(reason="uppercase rule not written yet"),
),
],
)
def test_problems(text, expected):
assert check_password(text) == expected
@pytest.mark.parametrize("suffix", ["", "!"])
@pytest.mark.parametrize("good", ["abcdef12", "pass1234", "9lives9lives"])
def test_good_passwords(good, suffix):
assert check_password(good + suffix) == []
@pytest.mark.parametrize(
"value,expectation",
[
pytest.param("abcdef12", nullcontext([]), id="str-ok"),
pytest.param("short", nullcontext(["too short", "no digit"]), id="str-problems"),
pytest.param(None, pytest.raises(TypeError), id="none"),
pytest.param(12345678, pytest.raises(TypeError, match="int"), id="int"),
],
)
def test_type_or_problems(value, expectation):
with expectation as expected:
assert check_password(value) == expected$ pytest -v test_password.py
collecting ... collected 17 items
test_password.py::test_problems[eight-chars-ok] PASSED [ 5%]
test_password.py::test_problems[seven-chars] PASSED [ 11%]
test_password.py::test_problems[letters-only] PASSED [ 17%]
test_password.py::test_problems[digits-only] PASSED [ 23%]
test_password.py::test_problems[empty] PASSED [ 29%]
test_password.py::test_problems[short-but-mixed] PASSED [ 35%]
test_password.py::test_problems[needs-uppercase] XFAIL (uppercase ru...) [ 41%]
test_password.py::test_good_passwords[abcdef12-] PASSED [ 47%]
test_password.py::test_good_passwords[abcdef12-!] PASSED [ 52%]
test_password.py::test_good_passwords[pass1234-] PASSED [ 58%]
test_password.py::test_good_passwords[pass1234-!] PASSED [ 64%]
test_password.py::test_good_passwords[9lives9lives-] PASSED [ 70%]
test_password.py::test_good_passwords[9lives9lives-!] PASSED [ 76%]
test_password.py::test_type_or_problems[str-ok] PASSED [ 82%]
test_password.py::test_type_or_problems[str-problems] PASSED [ 88%]
test_password.py::test_type_or_problems[none] PASSED [ 94%]
test_password.py::test_type_or_problems[int] PASSED [100%]
======================== 16 passed, 1 xfailed in 0.01s =========================इसे ऐसे क्यों लिखा गया:
- सीमा वाली जोड़ी सबसे पहले।
eight-chars-okऔरseven-charsलंबाई वाले नियम के दोनों ओर हैं, वही जगह जहाँ<बनाम<=की चूक दिखेगी। बाक़ी हर नियम को एक ऐसा केस मिलता है जो सिर्फ़ उसी नियम को तोड़ता है, ताकि फ़ेल होने पर उंगलीcheck_passwordकी ठीक एक लाइन पर जाए। emptyसूची के क्रम को जाँचता है। यही एक केस है जो तीनों नियम तोड़ता है, इसलिए यह पक्का करता है कि समस्याएँ एक तय क्रम में लौटती हैं — जिस पर उन्हें यूज़र को दिखाने वाला कोड निर्भर करेगा।- अपेक्षित वैल्यूज़ हर केस में नई सूचियाँ हैं, और टेस्ट उन्हें सिर्फ़ पढ़ता है। कुछ भी बदला नहीं जाता, इसलिए "जब यह काम न करे" वाला साझा-object का जाल यहाँ नहीं लग सकता।
test_good_passwordsमें स्टैकिंग ठीक बैठती है क्योंकि suffix और पासवर्ड स्वतंत्र हैं: कोई भी अच्छा पासवर्ड!के साथ या उसके बिना अच्छा ही रहना चाहिए।abcdef12-औरabcdef12-!जैसे ID बिना मदद के पढ़े जा सकते हैं, इसलिएids=की ज़रूरत नहीं पड़ी।- एरर वाली टेबल
pytest.paramids इस्तेमाल करती है क्योंकि वरनाnullcontextऔरpytest.raisesobjectsexpectation0,expectation1के रूप में दिखते।match="int"जाँचता है कि मैसेज ग़लत टाइप का नाम लेता है, सिर्फ़ यह नहीं कि कोईTypeErrorआया। xfailकेस एक भविष्य के नियम को दर्ज करता है, रन को तोड़े बिना। जब uppercase नियम जोड़ा जाएगा, वह केस पास होने लगेगा और pytest उसेXPASSरिपोर्ट करेगा — मार्क हटाने का संकेत।
ID को ऊपर से नीचे पढ़ने पर नियम मिल जाते हैं: आठ अक्षर, कम से कम एक अंक, कम से कम एक अक्षर, सिर्फ़ स्ट्रिंग्स, uppercase की योजना। नाम काफ़ी अच्छे हैं या नहीं, इसकी यही कसौटी है।
Step 6 of 6
चुनौती — the chapter quiz
सरल से कठिन — दस प्रश्न, अंतिम वाले जानबूझकर चुनौतीपूर्ण बनाए गए हैं।
Sign in to take the quiz