Marker, skip और xfail — कौन सा टेस्ट कहाँ चले
skip, skipif, importorskip और xfail से हर टेस्ट की ज़रूरत और उम्मीद बताना; strict=True, raises= और XPASS का मतलब; अपने marker रजिस्टर करके -m से चुनना; साथ में parametrize किया fixture और factory fixture।
- 1समस्या
- 2समझें
- 3उदाहरण
- 4अनुमान
- 5स्वयं करें
- 6चुनौती
वह समस्या जिसे हम हल कर रहे हैं
एक छोटा प्रोजेक्ट, shop, जिसमें एक मॉड्यूल और दो टेस्ट फ़ाइलें हैं। money.py:
def parse_price(text: str) -> float:
return float(text.replace(",", ""))
def split_bill(total: float, people: int) -> float:
return round(total / people, 2)test_money.py इसे सीधी-सादी वैल्यूज़ से जाँचती है। test_export.py कीमतें YAML से पढ़ती है, जिसके लिए बाहरी पैकेज yaml चाहिए:
import yaml
from money import parse_price
def test_prices_from_yaml():
data = yaml.safe_load("pen: '15.00'")
assert parse_price(data["pen"]) == 15.0जिस मशीन पर yaml इंस्टॉल नहीं है, वहाँ pytest -q कहता है:
==================================== ERRORS ====================================
_______________________ ERROR collecting test_export.py ________________________
ImportError while importing test module '/home/you/shop/test_export.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
/usr/lib/python3.12/importlib/__init__.py:90: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
test_export.py:1: in <module>
import yaml
E ModuleNotFoundError: No module named 'yaml'
=========================== short test summary info ============================
ERROR test_export.py
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.13sआख़िरी दो लाइनें पढ़िए। Interrupted। test_money.py के दोनों अच्छे टेस्ट चले ही नहीं। एक फ़ाइल, जो इस मशीन पर चल नहीं सकती थी, पूरे रन को अपने साथ ले डूबी।
जो भी प्रोजेक्ट काफ़ी समय तक चलता है, उसकी असली हालत यही होती है। कुछ टेस्ट को ऐसी लाइब्रेरी चाहिए जो वैकल्पिक है। कुछ का मतलब सिर्फ़ Linux पर बनता है। कुछ को ऐसी API key चाहिए जो सिर्फ़ CI के पास है। कुछ ऐसे बग का वर्णन करते हैं जिसके बारे में सब जानते हैं पर अभी तक किसी ने ठीक नहीं किया। और कुछ में एक मिनट लगता है, और हर बार फ़ाइल सेव करने पर आप एक मिनट इंतज़ार नहीं करना चाहते।
इन टेस्ट को मिटा देना ग़लत है, और कमेंट करके रख देना भी। आप जो चाहते हैं, वह है pytest को बताना — हर टेस्ट को क्या चाहिए और आप उससे क्या उम्मीद करते हैं — और फिर हर रन में क्या करना है, यह pytest को तय करने देना। इसका औज़ार है marker।
इस अध्याय के अंत में आप कर पाएंगे
- किसी टेस्ट को हमेशा, किसी शर्त पर, या टेस्ट के अंदर से skip करना, और कारण को दिखाई देने लायक रखना
- वैकल्पिक पैकेज न होने पर
pytest.importorskipसे पूरी फ़ाइल skip करना - किसी ज्ञात बग को
xfailसे चिह्नित करना, औरstrict=Trueवraises=से उसे सटीक बनाना - रिपोर्ट के अक्षर
s,xऔरXपढ़ना, और-rs,-rx,-rAसे विवरण माँगना - अपने marker बनाना, उन्हें रजिस्टर करना, और
--strict-markersसे टाइपो पकड़ना -m "not slow"और-m "slow and db"से टेस्ट चुनना- ऐसा fixture लिखना जो हर टेस्ट को कई बार चलाए, और ऐसा fixture जो एक फ़ंक्शन लौटाए
ज़रूरी शर्तें: mocking।
टेस्ट लिखने से पहले
Marker इस बारे में नहीं हैं कि टेस्ट क्या जाँचता है — वह पिछले अध्यायों में हो चुका। Marker दो सवालों के जवाब हैं, जो टेस्ट लिखने से पहले देने होते हैं, और जिनका जवाब ज़्यादातर लोग तब देते हैं जब कोई रन ग़लत वजह से लाल हो चुका होता है।
1. इस टेस्ट को चलने के लिए ही क्या चाहिए? कोई पैकेज जो अनिवार्य dependency नहीं है, कोई ऑपरेटिंग सिस्टम, पायथन का कोई वर्ज़न, कोई सीक्रेट रखने वाला environment variable, डेटाबेस, नेटवर्क, बहुत सारा समय। इनमें से हर एक एक पूर्व-शर्त है। अगर वह मौजूद नहीं है, तो टेस्ट के पास कहने को कुछ नहीं: वह न पास हुआ, न फ़ेल।
2. आज मैं इससे क्या उम्मीद करता हूँ? आम तौर पर, पास होना। लेकिन अगर कोड में कोई ज्ञात बग है और आप वह टेस्ट लिख रहे हैं जो सही व्यवहार बताता है, तो आप उम्मीद करते हैं कि यह फ़ेल होगा — और एक ख़ास तरीक़े से फ़ेल होगा।
जवाब ही marker तय करते हैं, इसलिए पहले उन्हें लिख लीजिए। shop प्रोजेक्ट के लिए योजना कुछ ऐसी दिखती है:
| केस | इनपुट / स्थिति | क्या चाहिए | अपेक्षित परिणाम | Marker | |---|---|---|---|---| | सादी कीमत | "1,250.50" | कुछ नहीं | 1250.5 | कोई नहीं | | YAML export | "pen: '15.00'" | yaml पैकेज | जहाँ इंस्टॉल है वहाँ 15.0 | pytest.importorskip("yaml") | | POSIX पाथ | "data", "prices.csv" | Windows नहीं | "data/prices.csv" | skipif(sys.platform == "win32") | | लाइव रेट | असली API | RATES_API_KEY सेट हो | एक रेट, सिर्फ़ जहाँ key है | टेस्ट के अंदर pytest.skip() | | मुद्रा चिह्न | "$1,250" | कुछ नहीं | 1250.0 — आज ValueError (बग #42) | xfail(raises=ValueError, strict=True) | | बहुत सी कीमतें | 1000 स्ट्रिंग | एक सेकंड या ज़्यादा | सही योग | slow (अपना) |
टेबल को कॉलम के हिसाब से पढ़िए। क्या चाहिए कॉलम से skip निकलते हैं। अपेक्षित परिणाम कॉलम से xfail निकलते हैं। और लगने वाले समय से एक अपना marker निकलता है, जिससे आप चुनते हैं कि क्या चलाना है।
इसमें से कुछ भी काम करे, उससे पहले क्या तैयार होना चाहिए:
pytestइंस्टॉल किया हुआ एक virtual environment, और टेस्ट सेmoney.pyimport हो सके (पिछले अध्यायों की तरह प्रोजेक्ट रूट से चलाइए);- प्रोजेक्ट रूट में एक
pyproject.toml, क्योंकि अपने marker वहीं रजिस्टर होते हैं; - बग कहीं एक नंबर के साथ लिखा हुआ, ताकि
reason=उसकी ओर इशारा कर सके।
और किसे marker नहीं देना है:
- किसी टेस्ट को इसलिए skip मत कीजिए कि वह फ़ेल होता है और आपको पता नहीं क्यों। Skip उस विफलता को आगे के हर रन से छिपा देता है। या तो उसे ठीक कीजिए, या उसे इतना समझिए कि कारण के साथ
xfail(raises=...)लिख सकें। - अस्थिर (flaky) टेस्ट को
xfailमत कीजिए। जो टेस्ट कभी-कभी पास होता है, वह किसी दिनXऔर किसी दिनxदिखाएगा, औरstrict=Trueके साथ बेतरतीब ढंग से फ़ेल होगा। अस्थिरता टेस्ट का अपना बग है; उसे ठीक कीजिए। - हर चीज़ को
slowका लेबल मत दीजिए। अगर हर रन में सुइट का ज़्यादातर हिस्सा बाहर हो जाए, तो रोज़ का रन कुछ नहीं जाँचता। सिर्फ़ उन गिने-चुने टेस्ट को लेबल दीजिए जो सच में सेकंड खाते हैं। - यह टेस्ट मत कीजिए कि pytest सही से skip करता है या नहीं। आप
money.pyको टेस्ट कर रहे हैं, pytest को नहीं। Marker कॉन्फ़िगरेशन है; रन की रिपोर्ट ही उसके काम करने का पर्याप्त सबूत है।
अध्याय का बाक़ी हिस्सा इसी टेबल को, पंक्ति-दर-पंक्ति, लागू करता है।
skip — यह टेस्ट नहीं, और कारण यह है
पहले वह तरीक़ा, जो marker के बारे में जानने से पहले आम तौर पर अपनाया जाता है — जल्दी return:
import os
def test_live_rates():
if "RATES_API_KEY" not in os.environ:
return
assert False, "would call the real API here". [100%]
1 passed in 0.07sPassed। टेस्ट ने कुछ भी नहीं जाँचा, और रिपोर्ट कहती है कि वह सफल रहा। बिना key वाले लैपटॉप पर यह हर रन में बोला गया एक झूठ है, और इस हरे बिंदु को असली बिंदु से अलग पहचानने का कोई तरीक़ा नहीं। हल शर्त से बचना नहीं, बल्कि उसे ईमानदारी से बताना है — skip के रूप में।
सबसे सरल marker एक decorator है जो कहता है "इसे मत चलाओ", साथ में एक कारण:
import os
import sys
import pytest
from money import parse_price, split_bill
@pytest.mark.skip(reason="rounding rules not agreed yet")
def test_split_rounding():
assert split_bill(100.0, 3) == 33.34
@pytest.mark.skipif(sys.platform == "win32", reason="uses a POSIX path")
def test_posix_path():
assert os.path.join("data", "prices.csv") == "data/prices.csv"
@pytest.mark.skipif(sys.version_info < (3, 13), reason="needs Python 3.13")
def test_new_feature():
assert parse_price("10") == 10.0
def test_live_rates():
if "RATES_API_KEY" not in os.environ:
pytest.skip("RATES_API_KEY is not set")
assert False, "would call the real API here"s.ss [100%]
1 passed, 3 skipped in 0.09sचार टेस्ट, फ़ैसला करने के चार अलग तरीक़े:
@pytest.mark.skip(reason=...)— हमेशा skip होता है। अंदर का कोड कभी नहीं चलता।@pytest.mark.skipif(condition, reason=...)— शर्त सच होने पर skip होता है। शर्त फ़ाइल को collect करते समय जाँची जाती है। यह रन Linux पर था, इसलिएtest_posix_pathचला और पास हुआ; पायथन 3.12 था, इसलिएtest_new_featureskip हुआ।- टेस्ट के अंदर बुलाया गया
pytest.skip("...")— फ़ैसला टेस्ट चलते समय होता है। इसे तब इस्तेमाल कीजिए जब आपको आधे रास्ते में पता चले, जैसे environment देखने के बाद या यह देखने के बाद कि किसी fixture ने क्या लौटाया।
प्रोग्रेस लाइन में हर s एक skip हुआ टेस्ट है। Skip विफलता नहीं है: रन हरा ही रहता है।
लेकिन कारण कहीं दिखाई नहीं देते। डिफ़ॉल्ट रूप से pytest सारांश में सिर्फ़ विफलताएँ और error दिखाता है। Skip देखने के लिए -rs दीजिए:
s.ss [100%]
=========================== short test summary info ============================
SKIPPED [1] test_skips.py:9: rounding rules not agreed yet
SKIPPED [1] test_skips.py:19: needs Python 3.13
SKIPPED [1] test_skips.py:26: RATES_API_KEY is not set
1 passed, 3 skipped in 0.10sइसीलिए reason मायने रखता है। तीन महीने बाद सिर्फ़ "skipped" शब्द किसी को नहीं बताएगा कि टेस्ट को फिर से चालू किया जा सकता है या नहीं।
importorskip — जब पूरी फ़ाइल को एक पैकेज चाहिए
वापस test_export.py पर। import yaml की जगह यह लिखिए:
import pytest
from money import parse_price
yaml = pytest.importorskip("yaml")
def test_prices_from_yaml():
data = yaml.safe_load("pen: '15.00'")
assert parse_price(data["pen"]) == 15.0.. [100%]
=========================== short test summary info ============================
SKIPPED [1] test_export.py:5: could not import 'yaml': No module named 'yaml'
2 passed, 1 skipped in 0.08spytest.importorskip("yaml") import करने की कोशिश करता है। सफल होने पर मॉड्यूल लौटाता है, इसीलिए उसे yaml में रखा गया है। असफल होने पर पूरी फ़ाइल skip हो जाती है, import की ग़लती को कारण बनाकर। बाक़ी दोनों टेस्ट अब चलते हैं। yaml इंस्टॉल कीजिए, और बिना किसी बदलाव के वही फ़ाइल पूरी चलेगी।
xfail — वह बग जिसके बारे में आप जानते हैं
Skip कहता है "यह टेस्ट यहाँ नहीं चल सकता"। एक दूसरी, अलग बात भी होती है: "यह टेस्ट चलता है, और इसका फ़ेल होना अपेक्षित है, क्योंकि कोड में एक ज्ञात बग है"।
parse_price में एक है। यह मुद्रा चिह्न को नहीं सँभालता:
import pytest
from money import parse_price
@pytest.mark.xfail(reason="bug #42: currency symbol is not stripped")
def test_parse_with_symbol():
assert parse_price("$1,250") == 1250.0
@pytest.mark.xfail(reason="spaces as thousands separators")
def test_parse_with_spaces():
assert parse_price("1 250") == 1250.0pytest -q -rx:
xx [100%]
=========================== short test summary info ============================
XFAIL test_xfail.py::test_parse_with_symbol - bug #42: currency symbol is not stripped
XFAIL test_xfail.py::test_parse_with_spaces - spaces as thousands separators
2 xfailed in 0.08sSkip हुए टेस्ट के उलट, xfail टेस्ट सच में चलता है। वह भविष्यवाणी के मुताबिक़ फ़ेल हुआ, इसलिए छोटा x दिखता है और रन हरा रहता है। टेस्ट अब बग का एक लिखित रिकॉर्ड है, जो सुइट में ही रखा है — किसी के दिमाग़ में रखा नोट नहीं।
तो सिर्फ़ skip क्यों नहीं? क्योंकि skip हुआ टेस्ट कभी चलता ही नहीं, इसलिए वह आपको कभी कुछ नहीं बता सकता — न तब जब बग ठीक हो जाए, न तब जब कोड बदलकर किसी नए तरीक़े से फ़ेल होने लगे। xfail टेस्ट हर बार चलता है, और उसका नतीजा आपकी भविष्यवाणी से मिलाया जाता है। Skip "यहाँ नहीं चल सकता" के लिए है; xfail "चलता है, और मुझे पता है यह क्या कहेगा" के लिए।
अब कोई बग #42 ठीक कर देता है और money.py बदलकर $ हटाने का इंतज़ाम कर देता है:
Xx [100%]
=================================== XPASSES ====================================
=========================== short test summary info ============================
XPASS test_xfail.py::test_parse_with_symbol - bug #42: currency symbol is not stripped
1 xfailed, 1 xpassed in 0.12sबड़ा X, XPASS: "फ़ेल होना था, पर पास हो गया"। और रन अब भी हरा है — exit code 0। हरे रन को कोई नहीं देखता, इसलिए marker बना रहता है, और टेस्ट एक ऐसे बग का दावा करता रहता है जो अब मौजूद ही नहीं।
strict=True — अच्छी ख़बर को ज़ोरदार बनाइए
import pytest
from money import parse_price
@pytest.mark.xfail(reason="bug #42: currency symbol is not stripped", strict=True)
def test_parse_with_symbol():
assert parse_price("$1,250") == 1250.0F [100%]
=================================== FAILURES ===================================
____________________________ test_parse_with_symbol ____________________________
[XPASS(strict)] bug #42: currency symbol is not stripped
=========================== short test summary info ============================
FAILED test_xfail.py::test_parse_with_symbol - [XPASS(strict)] bug #42: curre...
1 failed in 0.10sstrict=True के साथ, अप्रत्याशित पास एक विफलता है। सुनने में उल्टा लगता है, और ठीक यही आप चाहते हैं: जिसने बग ठीक किया उसे marker हटाने को कहा जाता है, और तब से टेस्ट एक सामान्य टेस्ट बनकर उस सुधार की रखवाली करता है। अगर प्रोजेक्ट के हर xfail के लिए यह चाहिए, तो pytest कॉन्फ़िगरेशन में xfail_strict = true लिखिए।
raises= — सही वजह से फ़ेल होइए
बिना आर्ग्युमेंट वाला xfail कोई भी विफलता मान लेता है। उनमें वे भी शामिल हैं जो आपका इरादा नहीं थीं:
import pytest
from money import parse_price
@pytest.mark.xfail(reason="bug #42")
def test_loose():
assert parse_prise("$1,250") == 1250.0
@pytest.mark.xfail(raises=ValueError, reason="bug #42")
def test_precise():
assert parse_prise("$1,250") == 1250.0xF [100%]
=================================== FAILURES ===================================
_________________________________ test_precise _________________________________
@pytest.mark.xfail(raises=ValueError, reason="bug #42")
def test_precise():
> assert parse_prise("$1,250") == 1250.0
^^^^^^^^^^^
E NameError: name 'parse_prise' is not defined
test_raises.py:13: NameError
=========================== short test summary info ============================
XFAIL test_raises.py::test_loose - bug #42
1 failed, 1 xfailed in 0.09sदोनों टेस्ट में एक ही टाइपो है, parse_prise। test_loose ने NameError को निगल लिया और शांति से x दिखा दिया — वह हमेशा के लिए "फ़ेल होना अपेक्षित" बना रहता, बिना कभी parse_price को टेस्ट किए। test_precise ने बता दिया कि वह कौन सी विफलता अपेक्षित मानता है — वह ValueError जो float("$1250") देता है — इसलिए कोई भी दूसरा exception असली विफलता के रूप में दिखाया जाता है। टाइपो ठीक कीजिए, और दोनों x हो जाएँगे।
रिपोर्ट के अक्षर
प्रोग्रेस लाइन हर टेस्ट के लिए एक अक्षर इस्तेमाल करती है, और सारांश में क्या जाए यह चुनने के लिए -r वही अक्षर लेता है:
| अक्षर | मतलब | सारांश में दिखाने के लिए | |---|---|---| | . | पास हुआ | -rp | | F | फ़ेल हुआ | -rf (डिफ़ॉल्ट रूप से चालू) | | E | setup या teardown में error | -rE (डिफ़ॉल्ट रूप से चालू) | | s | skip हुआ | -rs | | x | xfailed — उम्मीद के मुताबिक़ फ़ेल हुआ | -rx | | X | xpassed — अप्रत्याशित रूप से पास हुआ | -rX |
अक्षर जोड़े जा सकते हैं: -rsx skip और xfail दोनों एक साथ दिखाता है। -rA सब कुछ दिखाता है, पास हुए टेस्ट समेत:
..xs [100%]
==================================== PASSES ====================================
=========================== short test summary info ============================
PASSED test_params.py::test_parse_price[15-15.0]
PASSED test_params.py::test_parse_price[1,250.50-1250.5]
SKIPPED [1] test_params.py:6: space separators not decided
XFAIL test_params.py::test_parse_price[$1,250-1250.0] - bug #42
2 passed, 1 skipped, 1 xfailed in 0.08sअपने marker
skip और xfail बदलते हैं कि pytest किसी टेस्ट के साथ क्या करेगा। लेकिन marker एक सादा लेबल भी हो सकता है, आपका गढ़ा हुआ एक नाम, जो तब तक कुछ नहीं बदलता जब तक आप उससे टेस्ट न चुनें। मान लीजिए कुछ टेस्ट धीमे हैं, और कुछ को डेटाबेस चाहिए:
import time
import pytest
from money import parse_price, split_bill
def test_parse_plain():
assert parse_price("1,250.50") == 1250.5
@pytest.mark.slow
def test_parse_many():
time.sleep(1)
assert sum(parse_price("1.5") for _ in range(1000)) == 1500.0
@pytest.mark.slow
@pytest.mark.db
def test_bill_from_database():
time.sleep(1)
assert split_bill(300.0, 3) == 100.0
@pytest.mark.db
def test_bill_lookup():
assert split_bill(90.0, 3) == 30.0.... [100%]
=============================== warnings summary ===============================
test_marks.py:12
/home/you/shop/test_marks.py:12: PytestUnknownMarkWarning: Unknown pytest.mark.slow - is this a typo? You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/stable/how-to/mark.html
@pytest.mark.slow
test_marks.py:18
/home/you/shop/test_marks.py:18: PytestUnknownMarkWarning: Unknown pytest.mark.slow - is this a typo? You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/stable/how-to/mark.html
@pytest.mark.slow
test_marks.py:19
/home/you/shop/test_marks.py:19: PytestUnknownMarkWarning: Unknown pytest.mark.db - is this a typo? You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/stable/how-to/mark.html
@pytest.mark.db
test_marks.py:25
/home/you/shop/test_marks.py:25: PytestUnknownMarkWarning: Unknown pytest.mark.db - is this a typo? You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/stable/how-to/mark.html
@pytest.mark.db
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
4 passed, 4 warnings in 2.09sचारों पास हुए, और हर इस्तेमाल पर pytest शिकायत कर रहा है। pytest.mark.<कुछ भी> स्वीकार है — attribute उसी पल बन जाता है — इसलिए pytest नहीं बता सकता कि slow असली नाम है या किसी और चीज़ की ग़लत वर्तनी। वह चाहता है कि आप जिन नामों का मतलब रखते हैं, उन्हें घोषित करें।
Marker रजिस्टर करना
सूची pyproject.toml में जाती है, हर marker के लिए एक स्ट्रिंग, कोलन से पहले नाम और बाद में विवरण:
[tool.pytest.ini_options]
markers = [
"slow: takes more than a second; deselect with -m 'not slow'",
"db: needs the database",
].... [100%]
4 passed in 2.11sचेतावनियाँ ग़ायब हो गईं, और pytest --markers अब प्रोजेक्ट में आने वाले अगले व्यक्ति के लिए उन्हें सूचीबद्ध करता है:
@pytest.mark.slow: takes more than a second; deselect with -m 'not slow'
@pytest.mark.db: needs the database-m से चुनना
अब लेबल अपनी क़ीमत वसूलते हैं। -m marker के नामों पर एक expression लेता है, and, or, not और कोष्ठकों के साथ:
$ pytest -q -m "not slow"
.. [100%]
2 passed, 2 deselected in 0.07s
$ pytest -q -m "slow and db"
. [100%]
1 passed, 3 deselected in 1.08s
$ pytest -q -m "slow or db"
... [100%]
3 passed, 1 deselected in 2.07sDeselected का मतलब skipped नहीं है। Skip हुआ टेस्ट चुना गया था और फिर उसने चलने से इनकार किया; deselected टेस्ट कभी चुना ही नहीं गया। -m "not slow" वह रन है जो आप हर सेव पर करते हैं; पूरा रन CI के लिए और push करने से पहले के लिए है।
--strict-markers — टाइपो error बन जाता है
चेतावनी को अनदेखा करना आसान है। यह एक टेस्ट है जो किसी ने बाद में जोड़ा, marker की ग़लत वर्तनी के साथ:
@pytest.mark.slwo
def test_split_large_group():
time.sleep(1)
assert split_bill(1000.0, 8) == 125.0... [100%]
=============================== warnings summary ===============================
test_marks.py:30
/home/you/shop/test_marks.py:30: PytestUnknownMarkWarning: Unknown pytest.mark.slwo - is this a typo? You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/stable/how-to/mark.html
@pytest.mark.slwo
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
3 passed, 2 deselected, 1 warning in 1.07s-m "not slow" माँगा गया था, फिर भी रन में एक सेकंड लगा: ग़लत वर्तनी वाला टेस्ट slow नहीं है, इसलिए वह चला। --strict-markers के साथ, बिना रजिस्टर किया marker collection को ही रोक देता है:
==================================== ERRORS ====================================
________________________ ERROR collecting test_marks.py ________________________
'slwo' not found in `markers` configuration option
=========================== short test summary info ============================
ERROR test_marks.py - Failed: 'slwo' not found in `markers` configuration option
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.13sआप हर बार यह फ़्लैग टाइप नहीं करना चाहेंगे, इसलिए इसे addopts में रखिए:
[tool.pytest.ini_options]
addopts = "--strict-markers"
markers = [
"slow: takes more than a second; deselect with -m 'not slow'",
"db: needs the database",
]pytestmark — पूरी फ़ाइल को चिह्नित करना
जब किसी फ़ाइल के हर टेस्ट को डेटाबेस चाहिए, तो हर एक पर decorator लगाना दोहराव है। मॉड्यूल स्तर पर pytestmark नाम का एक वेरिएबल फ़ाइल के हर टेस्ट पर लागू होता है:
import pytest
from money import split_bill
pytestmark = pytest.mark.db
def test_bill_for_two():
assert split_bill(50.0, 2) == 25.0
def test_bill_for_four():
assert split_bill(80.0, 4) == 20.0$ pytest -q -m db
.. [100%]
2 passed, 4 deselected in 0.07sनाम ठीक-ठीक pytestmark ही होना चाहिए। यह एक लिस्ट भी हो सकता है, pytestmark = [pytest.mark.db, pytest.mark.slow]।
parametrize के एक अकेले केस पर marker
Parametrize किए गए टेस्ट पर marker हर केस पर लागू होता है। एक केस को चिह्नित करने के लिए उसे pytest.param(..., marks=...) में लपेटिए:
import pytest
from money import parse_price
@pytest.mark.parametrize(
"text, expected",
[
("15", 15.0),
("1,250.50", 1250.5),
pytest.param(
"$1,250", 1250.0,
marks=pytest.mark.xfail(raises=ValueError, reason="bug #42"),
),
pytest.param(
"1 250", 1250.0,
marks=pytest.mark.skip(reason="space separators not decided"),
),
],
)
def test_parse_price(text, expected):
assert parse_price(text) == expectedtest_params.py::test_parse_price[15-15.0] PASSED [ 25%]
test_params.py::test_parse_price[1,250.50-1250.5] PASSED [ 50%]
test_params.py::test_parse_price[$1,250-1250.0] XFAIL (bug #42) [ 75%]
test_params.py::test_parse_price[1 250-1250.0] SKIPPED (space separa...) [100%]
=================== 2 passed, 1 skipped, 1 xfailed in 0.11s ====================ज्ञात बग काम करने वाले केसों के बगल में, उसी टेबल में रहता है, और जिस दिन वह ठीक होगा, आप एक pytest.param का आवरण हटा देंगे।
और आगे: बदलने वाले fixture, और बनाने वाले fixture
दो fixture पैटर्न fixture वाले अध्यायों में नहीं समा पाए, और marker के बाद उनके लिए अच्छा मौक़ा है, क्योंकि दोनों उन्हीं टेस्ट को कई गुना करते हैं या आकार देते हैं जिन्हें चुनना आपने अभी सीखा।
Parametrize किया हुआ fixture
parametrize एक टेस्ट के इनपुट बदलता है। कभी-कभी आप चाहते हैं कि किसी fixture का इस्तेमाल करने वाला हर टेस्ट उस fixture की हर वैल्यू के लिए एक बार चले। @pytest.fixture को एक params लिस्ट दीजिए, और मौजूदा वैल्यू request.param से पढ़िए:
import pytest
from money import parse_price
@pytest.fixture(params=["1250", "1,250", "1,250.00"], ids=["plain", "comma", "decimals"])
def price_text(request):
return request.param
def test_parses_to_1250(price_text):
assert parse_price(price_text) == 1250.0
def test_is_positive(price_text):
assert parse_price(price_text) > 0test_fixtures.py::test_parses_to_1250[plain] PASSED [ 16%]
test_fixtures.py::test_parses_to_1250[comma] PASSED [ 33%]
test_fixtures.py::test_parses_to_1250[decimals] PASSED [ 50%]
test_fixtures.py::test_is_positive[plain] PASSED [ 66%]
test_fixtures.py::test_is_positive[comma] PASSED [ 83%]
test_fixtures.py::test_is_positive[decimals] PASSED [100%]
============================== 6 passed in 0.08s ===============================दो टेस्ट, तीन वैल्यू, छह रन। request एक बिल्ट-इन fixture है जो बताता है कि किस टेस्ट की तैयारी हो रही है; request.param सिर्फ़ तभी मौजूद होता है जब fixture के पास params हो। ids रिपोर्ट में हर वैल्यू को नाम देता है — इसके बिना pytest वैल्यू से ही id बनाता है, जो लंबी या न छपने लायक वैल्यू के लिए पढ़ना मुश्किल होता है। लिस्ट में marks= वाले pytest.param(...) भी हो सकते हैं, ठीक parametrize की तरह।
यह कहाँ काम आता है: वही टेस्ट कई backend, कई फ़ाइल फ़ॉर्मैट, कई मुद्राओं के ख़िलाफ़। params में एक वैल्यू जोड़िए, और fixture इस्तेमाल करने वाला हर टेस्ट उसे भी कवर करेगा।
Factory fixture
Fixture टेस्ट से आर्ग्युमेंट नहीं ले सकता; टेस्ट सिर्फ़ उसका नाम लेता है। तो जब किसी टेस्ट को अलग-अलग ढंग से बनी दो चीज़ें चाहिए, तब क्या करें? एक फ़ंक्शन लौटाइए:
import pytest
from money import parse_price
@pytest.fixture
def make_receipt(tmp_path):
def _make(name, lines):
path = tmp_path / f"{name}.txt"
path.write_text("\n".join(lines))
return path
return _make
def receipt_total(path):
return sum(parse_price(line) for line in path.read_text().splitlines())
def test_two_receipts(make_receipt):
lunch = make_receipt("lunch", ["15", "1,250.50"])
taxi = make_receipt("taxi", ["300"])
assert receipt_total(lunch) + receipt_total(taxi) == 1565.5
def test_empty_receipt(make_receipt):
empty = make_receipt("empty", [])
assert receipt_total(empty) == 0.. [100%]
2 passed in 0.08sFixture अब भी हर टेस्ट के लिए एक बार चलता है, और अब भी अपने fixture पाता है (यहाँ tmp_path, जो फ़ाइलें मिटाने का काम भी सँभालता है)। वह जो सौंपता है वह _make है, और टेस्ट उसे जितनी बार चाहे, जिन आर्ग्युमेंट के साथ चाहे, बुलाता है। make_... नाम आम परिपाटी है, और पढ़ने वाले को एक नज़र में बता देता है कि यह fixture बुलाने लायक एक फ़ंक्शन है, बनी-बनाई वैल्यू नहीं।
पूर्ण उदाहरण
सब कुछ एक साथ, एक प्रोजेक्ट में। pyproject.toml:
[tool.pytest.ini_options]
addopts = "--strict-markers"
markers = [
"slow: takes more than a second; deselect with -m 'not slow'",
"network: talks to an outside service",
]test_checkout.py, उसी money.py के साथ — जिसमें बग #42 अब भी है:
import os
import time
import pytest
from money import parse_price, split_bill
# Every test that asks for amount_text runs once per format.
@pytest.fixture(params=["300", "300.00", "1,250"], ids=["whole", "decimals", "comma"])
def amount_text(request):
return request.param
# A factory: the fixture hands back a function the test can call many times.
@pytest.fixture
def make_share():
def _make(text, people):
return split_bill(parse_price(text), people)
return _make
def test_share_is_positive(amount_text, make_share):
assert make_share(amount_text, 3) > 0
@pytest.mark.parametrize(
"text, people, share",
[
("300", 3, 100.0),
("90", 4, 22.5),
pytest.param(
"$300", 3, 100.0,
marks=pytest.mark.xfail(raises=ValueError, strict=True, reason="bug #42"),
),
],
)
def test_share(make_share, text, people, share):
assert make_share(text, people) == share
@pytest.mark.slow
def test_many_group_sizes(make_share):
time.sleep(1) # stands in for real, slow work
assert all(make_share("300", n) > 0 for n in range(1, 1000))
@pytest.mark.network
def test_live_exchange_rate():
if "RATES_API_KEY" not in os.environ:
pytest.skip("RATES_API_KEY is not set")
raise AssertionError("would call the real service here")रोज़ का रन, कारणों के साथ:
$ pytest -q -rsx -m "not slow"
.....xs [100%]
=========================== short test summary info ============================
SKIPPED [1] test_checkout.py:52: RATES_API_KEY is not set
XFAIL test_checkout.py::test_share[$300-3-100.0] - bug #42
5 passed, 1 skipped, 1 deselected, 1 xfailed in 0.09sसिर्फ़ धीमा टेस्ट, और ऑफ़लाइन रन जो धीमे और नेटवर्क वाले दोनों तरह के टेस्ट छोड़ देता है:
$ pytest -q -m slow
. [100%]
1 passed, 7 deselected in 1.07s
$ pytest -q -m "not slow and not network"
.....x [100%]
5 passed, 2 deselected, 1 xfailed in 0.08sगिनिए: तीन वैल्यू वाले एक fixture से test_share_is_positive के तीन रन, test_share के तीन केस, एक धीमा टेस्ट और एक नेटवर्क टेस्ट — कुल आठ, और 1 passed, 7 deselected का जोड़ भी यही है।
और हर नतीजा सच बोलता है। नेटवर्क टेस्ट बिना key के पास होने का दिखावा नहीं करता; वह बताता है कि skip हुआ और क्यों। बग #42 सुइट में है, और क्योंकि वह raises=ValueError के साथ strict है, जिस दिन वह ठीक होगा उस दिन रन लाल होकर marker हटाने को कहेगा।
जब यह काम न करे
PytestUnknownMarkWarning: Unknown pytest.mark.slow - is this a typo? Marker रजिस्टर नहीं है। उसे pyproject.toml के markers में जोड़िए। अगर नाम सच में टाइपो है तो उसे ठीक कीजिए — और addopts में --strict-markers जोड़िए, ताकि अगला टाइपो चेतावनी नहीं, error बने।
`'slwo' not found in markers configuration option` यह --strict-markers का अपना काम करना है: फ़ाइल ऐसा marker इस्तेमाल कर रही है जो सूची में नहीं है। वर्तनी ठीक कीजिए, या नया नाम रजिस्टर कीजिए।
8 deselected और no tests ran, exit code 5 -m किसी से मेल नहीं खाया। -m expression में ग़लत वर्तनी वाला नाम error नहीं है, --strict-markers के साथ भी नहीं — -m sloww बस कोई टेस्ट नहीं चुनता। नाम को pytest --markers से मिलाइए।
ERROR: Wrong expression passed to '-m': not slow and: at column 13: expected not OR left parenthesis OR identifier; got end of input -m expression अधूरा है। पूरे expression को कोट्स में रखिए, और पक्का कीजिए कि हर and/or के दोनों तरफ़ कुछ हो।
Error evaluating 'skipif': you need to specify reason=STRING when using booleans as conditions. बिना reason= के skipif(sys.platform == "linux")। Boolean शर्त कारण के बारे में कुछ नहीं बताती, इसलिए pytest कारण पर ज़ोर देता है।
`Using pytest.skip outside of a test will skip the entire module. If that's your intention, pass allow_module_level=True.` pytest.skip("...") फ़ाइल के सबसे ऊपरी स्तर पर बुलाया गया है। पैकेज न हो तो pytest.importorskip इस्तेमाल कीजिए; वरना pytest.skip("...", allow_module_level=True) लिखिए, या pytestmark में एक skipif रखिए।
[XPASS(strict)] bug #42 अच्छी ख़बर, विफलता के रूप में बताई गई: बग ठीक हो गया है। xfail marker हटाइए और टेस्ट रखिए।
बग ठीक होने के बाद भी x बना रहने वाला xfail टेस्ट वह किसी और वजह से फ़ेल हो रहा है — अक्सर टेस्ट के अंदर ही कोई टाइपो। जिस exception की आप सच में उम्मीद करते हैं उसके साथ raises= जोड़िए, और बाक़ी सब असली विफलता के रूप में दिखेगा।
AttributeError: 'SubRequest' object has no attribute 'param' Fixture request.param पढ़ रहा है, लेकिन उसके पास कोई params= लिस्ट नहीं है, और उसे किसी और तरीक़े से parametrize भी नहीं किया गया। @pytest.fixture में params=[...] जोड़िए।
fixture 'name' not found, किसी fixture की ओर इशारा करते हुए Fixture को def make_user(name): लिखा गया, इस उम्मीद में कि टेस्ट name भेजेगा। Fixture के आर्ग्युमेंट दूसरे fixture होते हैं, इसलिए pytest name नाम का fixture ढूँढने चला गया। इसे factory बनाइए: एक अंदरूनी फ़ंक्शन जो name लेता है, और जिसे fixture लौटाता है।
चरण 4 / 6 — अनुमान
अपनी समझ की जाँच करें
pytest -q चलाने पर प्रोग्रेस लाइन और आख़िरी लाइन क्या होगी? (याद रखिए, round(2.675, 2) देता है 2.67।)
import pytest
@pytest.mark.skip(reason="not ready")
def test_export():
assert False
def test_total():
assert 2 + 2 == 4
@pytest.mark.xfail(reason="bug #7")
def test_rounding():
assert round(2.675, 2) == 2.68- As.F 1 passed, 1 skipped, 1 failed
- B..x 2 passed, 1 xfailed
- Cs.x 1 passed, 1 skipped, 1 xfailed
- Ds.X 1 passed, 1 skipped, 1 xpassed
slow marker pyproject.toml में रजिस्टर है, और --strict-markers बंद है। आप pytest -m "not slow" चलाते हैं, फिर भी रन में पाँच सेकंड लगते हैं। क्यों?
import time
import pytest
@pytest.mark.slwo
def test_big_report():
time.sleep(5)
assert True- A`-m` सिर्फ़ `skip` जैसे बिल्ट-इन marker के साथ काम करता है
- BMarker की वर्तनी ग़लत है, `slwo` — इसलिए टेस्ट `slow` नहीं है और चुन लिया गया; pytest सिर्फ़ चेतावनी देता है
- C`time.sleep` होने पर pytest marker को अनदेखा कर देता है
- D`-m "not slow"` सिर्फ़ धीमे टेस्ट चलाता है
बग #12 असल में पहले ही ठीक हो चुका है, इसलिए assert सच है। pytest -q क्या बताएगा?
import pytest
@pytest.mark.xfail(strict=True, reason="bug #12")
def test_upper():
assert "taka".upper() == "TAKA"- A`X` — 1 xpassed, exit code 0
- B`x` — 1 xfailed, exit code 0
- C`s` — marker होने से टेस्ट skip हो जाता है
- D`F` — `[XPASS(strict)]` के साथ FAILED, exit code 1
उत्तर देने के लिए अकाउंट आवश्यक है
अपने उत्तर जाँचने के लिए साइन इन करें
प्रश्न ऊपर दिए गए हैं, और मन में उत्तर सोचना ही मुख्य कार्य है। सही उत्तर, व्याख्या और तीन-स्तरीय संकेत देखने के लिए साइन इन करें।
आपकी बारी
कोई टेस्ट सुइट लीजिए जो आपके पास पहले से है — किसी पिछले अध्याय से, या अपने काम से — और उसे marker का एक ईमानदार सेट दीजिए:
pyproject.tomlमें अपने दो marker रजिस्टर कीजिए (जैसेslowऔरnetwork), औरaddoptsमें--strict-markersचालू कीजिए।- हर एक से कम से कम एक टेस्ट चिह्नित कीजिए। अगर कोई धीमा टेस्ट नहीं है, तो
time.sleep(1)से एक बना लीजिए। - ऐसा टेस्ट जोड़िए जिसे एक environment variable चाहिए, और variable न होने पर अंदर से
pytest.skipसे उसे skip कीजिए। - अपने कोड में एक बग ढूँढिए, या गढ़ लीजिए। वह टेस्ट लिखिए जिसे पास होना चाहिए, और उसे
strict=Trueऔर एक सटीकraises=के साथxfailदीजिए। paramsऔरidsवाला एक fixture जोड़िए, जिसे कम से कम दो टेस्ट इस्तेमाल करें।
फिर चलाइए, और हर एक का आउटपुट सहेज लीजिए:
pytest -q -rsx -m "not slow"pytest -q -m slowpytest -q -rA
आख़िर में, दो प्रयोग। अपने किसी marker की वर्तनी ग़लत कीजिए और एक बार --strict-markers के बिना और एक बार उसके साथ चलाइए। फिर चरण 4 का बग ठीक कीजिए और दोबारा चलाइए। हर बार pytest क्या कहता है, पढ़िए — यही दो संदेश marker वाले सुइट को ईमानदार रखते हैं, और एक बार ख़ुद पैदा करने के बाद दूसरी बार इन्हें पहचानना आसान होता है।
समाधान
एक समाधान, shop प्रोजेक्ट के money.py के साथ। चरण 4 का बग नया है, बग #51: स्प्रेडशीट का एक ख़ाली सेल "" बनकर आता है, और उसे 0.0 गिना जाना चाहिए — लेकिन float("") ValueError देता है।
pyproject.toml:
[tool.pytest.ini_options]
addopts = "--strict-markers"
markers = [
"slow: takes more than a second; deselect with -m 'not slow'",
"network: needs a real outside service and its API key",
]test_solution.py:
import os
import time
import pytest
from money import parse_price, split_bill
# Step 5: one fixture, three formats, used by two tests.
@pytest.fixture(params=["90", "90.00", "1,090"], ids=["whole", "decimals", "comma"])
def amount_text(request):
return request.param
def test_parse_is_positive(amount_text):
assert parse_price(amount_text) > 0
def test_split_for_one_is_whole_amount(amount_text):
assert split_bill(parse_price(amount_text), 1) == parse_price(amount_text)
# Step 2: a slow test, labelled so it can be left out.
@pytest.mark.slow
def test_many_prices():
time.sleep(1) # stands in for real, slow work
assert sum(parse_price("2.5") for _ in range(1000)) == 2500.0
# Step 3: decided inside the test, because only the environment knows.
@pytest.mark.network
def test_live_rates():
if "RATES_API_KEY" not in os.environ:
pytest.skip("RATES_API_KEY is not set")
raise AssertionError("would call the real service here")
# Step 4: bug #51 - an empty cell should count as 0, today it raises ValueError.
@pytest.mark.xfail(raises=ValueError, strict=True, reason="bug #51: empty cell")
def test_empty_cell_is_zero():
assert parse_price("") == 0.0तीनों रन:
$ pytest -q -rsx -m "not slow"
......sx [100%]
=========================== short test summary info ============================
SKIPPED [1] test_solution.py:34: RATES_API_KEY is not set
XFAIL test_solution.py::test_empty_cell_is_zero - bug #51: empty cell
6 passed, 1 skipped, 1 deselected, 1 xfailed in 0.09s
$ pytest -q -m slow
. [100%]
1 passed, 8 deselected in 1.07s
$ pytest -q -rA
.......sx [100%]
==================================== PASSES ====================================
=========================== short test summary info ============================
PASSED test_solution.py::test_parse_is_positive[whole]
PASSED test_solution.py::test_parse_is_positive[decimals]
PASSED test_solution.py::test_parse_is_positive[comma]
PASSED test_solution.py::test_split_for_one_is_whole_amount[whole]
PASSED test_solution.py::test_split_for_one_is_whole_amount[decimals]
PASSED test_solution.py::test_split_for_one_is_whole_amount[comma]
PASSED test_solution.py::test_many_prices
SKIPPED [1] test_solution.py:34: RATES_API_KEY is not set
XFAIL test_solution.py::test_empty_cell_is_zero - bug #51: empty cell
7 passed, 1 skipped, 1 xfailed in 1.08sपहला प्रयोग: @pytest.mark.slow की जगह ग़लत वर्तनी में @pytest.mark.slwo। --strict-markers के बिना (यहाँ -o addopts="" से बंद किया गया), रन में अब भी एक सेकंड लगता है, क्योंकि "धीमा" टेस्ट बाहर नहीं हुआ:
$ pytest -q -m "not slow" -o addopts=""
.......sx [100%]
=============================== warnings summary ===============================
test_solution.py:24
/home/you/shop/test_solution.py:24: PytestUnknownMarkWarning: Unknown pytest.mark.slwo - is this a typo? You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/stable/how-to/mark.html
@pytest.mark.slwo
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
7 passed, 1 skipped, 1 xfailed, 1 warning in 1.09sउसके साथ, रन शुरू ही नहीं होता:
$ pytest -q -m "not slow"
==================================== ERRORS ====================================
______________________ ERROR collecting test_solution.py _______________________
'slwo' not found in `markers` configuration option
=========================== short test summary info ============================
ERROR test_solution.py - Failed: 'slwo' not found in `markers` configuration ...
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.13sदूसरा प्रयोग: money.py में बग #51 ठीक किया गया:
def parse_price(text: str) -> float:
if not text.strip():
return 0.0
return float(text.replace(",", ""))
def split_bill(total: float, people: int) -> float:
return round(total / people, 2)$ pytest -q -m "not slow"
......sF [100%]
=================================== FAILURES ===================================
___________________________ test_empty_cell_is_zero ____________________________
[XPASS(strict)] bug #51: empty cell
=========================== short test summary info ============================
FAILED test_solution.py::test_empty_cell_is_zero - [XPASS(strict)] bug #51: e...
1 failed, 6 passed, 1 skipped, 1 deselected in 0.08sxfail वाली लाइन हटाइए, और तब से टेस्ट एक सामान्य टेस्ट की तरह पास होगा।
हर फ़ैसले की वजह:
--strict-markersaddoptsमें, कमांड लाइन पर नहीं। जिस सुरक्षा-जाल को चालू करना याद रखना पड़े, वह सुरक्षा-जाल नहीं है। कॉन्फ़िग में होने पर यह सब पर लागू होता है, CI समेत।slowएक लेबल, skip नहीं। धीमा टेस्ट एक अच्छा टेस्ट है; उसे चलना चाहिए, बस हर सेव पर नहीं। लेबल आपको चुनने देता है; skip उसे हर रन से हटा देता।- API key के लिए टेस्ट के अंदर
pytest.skip(),skipifनहीं। यहाँ दोनों काम करते, लेकिन जाँच उसी कोड के पास होनी चाहिए जिसे key चाहिए। और जब टेस्ट को आधे रास्ते में पता चलता है कि वह आगे नहीं बढ़ सकता, तब अंदर वाला रूप ही एकमात्र तरीक़ा है। raises=ValueErrorऔरstrict=Trueएक साथ।raises=पक्का करता है कि टेस्ट बग #51 की वजह से फ़ेल हो रहा है, किसी टाइपो की वजह से नहीं;strict=Trueपक्का करता है कि बग ठीक होने वाला दिन नज़र में आए। हर एक marker के झूठ बोलने का एक अलग रास्ता बंद करता है।- फ़ॉर्मैट के लिए parametrize किया हुआ fixture,
parametrizeनहीं। दो टेस्ट वही तीन इनपुट साझा करते हैं। Fixture में सूची एक जगह रहती है; चौथा फ़ॉर्मैट जोड़ने पर वह दोनों टेस्ट में एक साथ जुड़ जाता है। - Id
whole,decimals,comma। रिपोर्ट में[comma]बता देता है कि केस किस बारे में है। यहाँ[1,090]भी चल जाता, लेकिन वैल्यू लंबी या न छपने लायक हो तब भी id काम करती रहती है।
Step 6 of 6
चुनौती — the chapter quiz
सरल से कठिन — दस प्रश्न, अंतिम वाले जानबूझकर चुनौतीपूर्ण बनाए गए हैं।
Sign in to take the quiz