टेस्ट discovery और टेस्ट चलाना — pytest क्या चलाता है, और कैसे चुनें
pytest किन फ़ाइलों, फ़ंक्शनों और क्लासों को टेस्ट मानता है, src/ और tests/ layout में कोड import करना, node ID, और -k, -x, --lf, --ff, --co से ठीक वही चलाना जिसकी ज़रूरत है।
- 1समस्या
- 2समझें
- 3उदाहरण
- 4अनुमान
- 5स्वयं करें
- 6चुनौती
वह समस्या जिसे हम हल कर रहे हैं
अब तक हर टेस्ट एक ही फ़ाइल में रहा है, उस कोड के ठीक बगल में जिसे वह टेस्ट करता है, और सिर्फ़ pytest लिखना काफ़ी रहा है। असली प्रोजेक्ट इतने छोटे नहीं रहते। कोड एक पैकेज में चला जाता है, टेस्ट अपने अलग फ़ोल्डर में चले जाते हैं, और ठीक अगला रन कुछ ऐसा दिखता है:
$ pytest -q
==================================== ERRORS ====================================
_____________________ ERROR collecting tests/test_cart.py ______________________
ImportError while importing test module '/home/you/shop/tests/test_cart.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)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_cart.py:1: in <module>
from shop.cart import Cart
E ModuleNotFoundError: No module named 'shop'
=========================== short test summary info ============================
ERROR tests/test_cart.py
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.01sएक भी टेस्ट नहीं चला। कोड में कोई खराबी नहीं है; pytest बस उसे ढूँढ नहीं पाया।
यह ठीक होते ही दूसरी समस्या आती है: अब दर्जनों टेस्ट हैं, उनमें से एक फ़ेल हो रहा है, और हर बार दोबारा चलाने पर सारे के सारे फिर चलते हैं। आप सिर्फ़ उसी एक को चलाना चाहते हैं — या सिर्फ़ उन्हें जो पिछली बार फ़ेल हुए थे।
दोनों समस्याओं की जड़ एक ही है। कुछ भी चलाने से पहले pytest collect करता है: फ़ोल्डरों में घूमता है, फ़ाइलें import करता है, और टेस्ट की एक सूची बनाता है। यह अध्याय इसी के बारे में है कि वह सूची कैसे बनती है, और उसमें से चुनाव कैसे किया जाता है।
इस अध्याय के अंत में आप कर पाएंगे
- वे नियम बताना जिनसे
pytestतय करता है कि टेस्ट क्या है src/औरtests/के साथ प्रोजेक्ट को व्यवस्थित करना, और टेस्ट को कोड import करने लायक बनाना- टेस्ट को क्लास में समूहित करना, और
tests/test_cart.py::TestCart::test_total_adds_taxजैसा node ID पढ़ना - एक फ़ाइल, एक क्लास, एक टेस्ट, या
-kसे चुने गए टेस्ट चलाना - रोज़ के काम में
-x,--maxfail,--lf,--ff,--coऔर--durationsका उपयोग करना pytestका exit code पढ़कर उसका अर्थ बताना
ज़रूरी शर्तें: assert और failure रिपोर्ट।
टेस्ट लिखने से पहले
इस अध्याय में टेस्ट कोड की पहली लाइन से पहले का सवाल सिर्फ़ यह नहीं है कि क्या टेस्ट करना है, बल्कि यह भी कि टेस्ट कहाँ रहेगा और `pytest` उसे कैसे ढूँढेगा। जो टेस्ट कभी collect ही नहीं होता, वह टेस्ट न होने से भी बुरा है: वह सुरक्षा जैसा दिखता है, पर होता नहीं।
1. वादे को जानिए। जिस कोड को हम टेस्ट करेंगे, वह एक छोटी दुकान का पैकेज है:
apply_discount(price, percent)कीमत कोpercentजितना घटाकर, 2 दशमलव तक round करके लौटाता है। 0–100 से बाहर का percentValueErrorउठाता है।add_tax(price, rate=0.15)कीमत में टैक्स जोड़कर, 2 दशमलव तक round करके लौटाता है।Cart(name, price, quantity)आइटम जमा करता है;subtotal()quantity से गुणा करता है;total()टैक्स सहित subtotal है।
2. सेटअप तैयार रखिए। pytest इंस्टॉल किया हुआ एक virtual environment; src/shop/ में रखा कोड; और — वह हिस्सा जिसके बारे में यह अध्याय है — टेस्ट के लिए import shop कर पाने का कोई तरीका। आख़िरी हिस्सा न हो, तो बाकी कुछ मायने नहीं रखता।
3. केसों की योजना बनाइए, और तय कीजिए कि हर केस कहाँ रहेगा। हर मॉड्यूल के लिए एक टेस्ट फ़ाइल (tests/test_pricing.py, src/shop/pricing.py का प्रतिबिंब है), और cart के टेस्ट एक क्लास में। योजना में node ID लिखने से यह फ़ैसला पहले ही लेना पड़ता है:
| केस | इनपुट | अपेक्षित | Node ID | |---|---|---|---| | सामान्य छूट | apply_discount(200.0, 10) | 180.0 | tests/test_pricing.py::test_discount_ten_percent | | सीमा: कोई छूट नहीं | apply_discount(200.0, 0) | 200.0 | tests/test_pricing.py::test_discount_zero | | डिफ़ॉल्ट टैक्स | add_tax(100.0) | 115.0 | tests/test_pricing.py::test_add_tax_default_rate | | किनारा: खाली cart | Cart().subtotal() | 0 | tests/test_cart.py::TestCart::test_empty_cart_subtotal | | quantity गिनी जाती है | 15.0 के 3 पेन | 45.0 | tests/test_cart.py::TestCart::test_subtotal_counts_quantity | | total में टैक्स शामिल | 100.0 का 1 बैग | 115.0 | tests/test_cart.py::TestCart::test_total_adds_tax | | अमान्य percent | apply_discount(200.0, 150) | ValueError | बाद में — exception टेस्ट करने का अपना अध्याय है |
4. तय कीजिए कि क्या टेस्ट नहीं करना है। round() या list.append को टेस्ट मत कीजिए — पायथन का अपना व्यवहार आपका वादा नहीं है। यह टेस्ट मत कीजिए कि Cart.items tuples की लिस्ट है; वह अंदरूनी ब्योरा है जिसे आप कल बदल सकते हैं। वह टेस्ट कीजिए जो इस्तेमाल करने वाला देखता है: subtotal() और total()।
अब यह अध्याय ठीक इसी योजना को बनाता है, और रास्ते की हर समस्या discovery की समस्या है।
Discovery के नियम
pytest जो कुछ भी मिले, उसे नहीं चलाता। वह चार नियम मानता है, और उनके बाहर की हर चीज़ बिना एक शब्द कहे अनदेखी कर देता है:
- फ़ाइलें, जिनका नाम
test_*.pyया*_test.pyहो - उन फ़ाइलों के फ़ंक्शन, जिनका नाम
testसे शुरू हो - क्लास, जिनका नाम
Testसे शुरू हो — और जिनमें कोई__init__मेथड न हो - ऐसी क्लास के अंदर के मेथड, जिनका नाम
testसे शुरू हो
"हर फ़ंक्शन चला दो" के बजाय नियम क्यों? क्योंकि आपका प्रोजेक्ट ऐसे फ़ंक्शनों से भरा है जो टेस्ट नहीं हैं — helper, और ख़ुद वह कोड जिसे टेस्ट किया जा रहा है — और pytest को आपसे कुछ भी रजिस्टर करवाए बिना इन्हें अलग पहचानने का तरीका चाहिए।
नियमों पर भरोसा करने का सबसे तेज़ तरीका है उन्हें जानबूझकर तोड़ना। test_rules.py:
def test_found():
assert True
def check_not_found():
assert False
class TestGroup:
def test_method_found(self):
assert True
def helper(self):
assert False
class CartTests:
def test_ignored(self):
assert False
class TestWithInit:
def __init__(self):
self.value = 1
def test_never_runs(self):
assert self.value == 1इसके बगल में rules_test.py में एक पास होने वाला test_suffix_style है, और helpers.py में एक test_in_helpers है जो assert False करता है। अब pytest -v:
collecting ... collected 3 items
rules_test.py::test_suffix_style PASSED [ 33%]
test_rules.py::test_found PASSED [ 66%]
test_rules.py::TestGroup::test_method_found PASSED [100%]
=============================== warnings summary ===============================
test_rules.py:22
/home/you/disc/test_rules.py:22: PytestCollectionWarning: cannot collect test class 'TestWithInit' because it has a __init__ constructor (from: test_rules.py)
class TestWithInit:
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
========================= 3 passed, 1 warning in 0.01s =========================फ़ोल्डर में पाँच assert False लाइनें हैं, और रन हरा है। क्या छूटा, और क्यों:
check_not_found— नामtestसे शुरू नहीं होता।TestGroup.helper— टेस्ट क्लास के अंदर है, पर उसका अपना नामtestसे शुरू नहीं होता। Helper मेथड रखे जा सकते हैं; बस वे टेस्ट नहीं हैं।CartTests— क्लास का नामTestसे शुरू होना चाहिए।TestWithInit—pytestको क्लास ख़ुद, बिना किसी argument के, हर टेस्ट के लिए एक बार बनानी होती है। जिस क्लास का अपना__init__हो, उस पर इसके लिए भरोसा नहीं किया जा सकता, इसलिए वह छोड़ दी जाती है — एक warning के साथ, पूरे रन में इकलौती आवाज़।helpers.py— फ़ाइल का नाम किसी pattern से मेल नहीं खाता, इसलिए वह कभी import ही नहीं होती।
ख़ामोश हिस्सा ही ख़तरनाक हिस्सा है। अगर ग़लती सेdef tset_total():लिख दें, तो suite हमेशा हरा रहेगा। जब कोई नया टेस्ट संदिग्ध रूप से आसानी से "पास" हो, तो जाँचिए कि वह collect हुआ भी या नहीं — नीचे का--coयही करता है।
एक असली ढाँचा: src/ और tests/
shop/
├── pyproject.toml
├── src/
│ └── shop/
│ ├── __init__.py
│ ├── cart.py
│ └── pricing.py
└── tests/
├── test_cart.py
└── test_pricing.pyकोड src/shop/ में रहता है; टेस्ट tests/ में रहते हैं और पैकेज के साथ कभी ship नहीं होते। इसे src layout कहते हैं। इसका मक़सद यह है कि सिर्फ़ इसलिए कि आप प्रोजेक्ट फ़ोल्डर में खड़े हैं, कोड "ग़लती से" import न हो जाए — ताकि टेस्ट उसे ठीक वैसे import करें जैसे कोई असली user करेगा।
src/shop/pricing.py:
def apply_discount(price, percent):
if not 0 <= percent <= 100:
raise ValueError(f"percent must be between 0 and 100: {percent}")
return round(price * (100 - percent) / 100, 2)
def add_tax(price, rate=0.15):
return round(price * (1 + rate), 2)src/shop/cart.py:
from shop.pricing import add_tax
class Cart:
def __init__(self):
self.items = []
def add(self, name, price, quantity=1):
self.items.append((name, price, quantity))
def subtotal(self):
return sum(price * quantity for _, price, quantity in self.items)
def total(self):
return add_tax(self.subtotal())tests/test_pricing.py — योजना की पहली तीन पंक्तियाँ:
from shop.pricing import add_tax, apply_discount
def test_discount_ten_percent():
assert apply_discount(200.0, 10) == 180.0
def test_discount_zero():
assert apply_discount(200.0, 0) == 200.0
def test_add_tax_default_rate():
assert add_tax(100.0) == 115.0यही from shop.pricing import ... अध्याय की शुरुआत वाले ModuleNotFoundError का कारण था।
shop क्यों नहीं मिलता
import shop तभी सफल होता है जब sys.path के किसी फ़ोल्डर में shop फ़ोल्डर हो। पैकेज src/ में है, और src/ sys.path में नहीं है। pytest हर टेस्ट फ़ाइल का फ़ोल्डर (यहाँ tests/) जोड़ता है ताकि टेस्ट अपने पड़ोसियों को import कर सके — पर src/ कोई नहीं जोड़ता।
ग़लत तरीका है टेस्ट फ़ाइल के अंदर sys.path में जुगाड़ करना:
import sys
sys.path.insert(0, "src")
from shop.cart import Cart
def test_empty_cart_subtotal():
assert Cart().subtotal() == 0प्रोजेक्ट फ़ोल्डर से यह पास होता है। फिर कोई इसे tests/ के अंदर से चलाता है:
$ cd tests
$ pytest -q
==================================== ERRORS ====================================
________________________ ERROR collecting test_cart.py _________________________
ImportError while importing test module '/home/you/shop/tests/test_cart.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_cart.py:5: in <module>
from shop.cart import Cart
E ModuleNotFoundError: No module named 'shop'
=========================== short test summary info ============================
ERROR test_cart.py
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.01s"src" एक relative path है — आप जहाँ भी हों वहाँ से गिना जाता है, इसलिए अब टेस्ट इस पर निर्भर है कि आप किस फ़ोल्डर में खड़े होकर कमांड लिख रहे हैं। और हर टेस्ट फ़ाइल में वही तीन लाइनें चाहिए। सही तरीके दो हैं।
सही तरीका 1: pytest को बताइए कि कोड कहाँ है। pyproject.toml में:
[tool.pytest.ini_options]
pythonpath = ["src"]
testpaths = ["tests"]pythonpath किसी भी टेस्ट के import होने से पहले src/ को sys.path में जोड़ देता है — प्रोजेक्ट root से गिनकर, न कि आप जहाँ खड़े हैं वहाँ से। testpaths बताता है कि सिर्फ़ pytest लिखने पर कहाँ देखना है। अब:
$ pytest
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0
rootdir: /home/you/shop
configfile: pyproject.toml
testpaths: tests
collected 6 items
tests/test_cart.py ... [ 50%]
tests/test_pricing.py ... [100%]
============================== 6 passed in 0.01s ===============================हेडर में configfile: pyproject.toml इस बात का सबूत है कि pytest ने आपकी settings पढ़ीं। Configuration फ़ाइलों का आगे अपना अध्याय है; अभी ये दो लाइनें ही काफ़ी हैं।
सही तरीका 2: पैकेज इंस्टॉल कीजिए। अगर pyproject.toml एक असली पैकेज का वर्णन करता है (एक [build-system] और एक [project] टेबल), तो एक बार pip install -e . चलाइए। यह पैकेज को editable मोड में इंस्टॉल करता है: अब import shop हर जगह काम करता है — टेस्ट में, REPL में, scripts में — और src/ में बदलाव बिना दोबारा इंस्टॉल किए दिख जाते हैं। इसके बाद बिना किसी pythonpath के pytest -q वही 6 passed देता है। ज़्यादातर पैकेज वाले प्रोजेक्ट यही करते हैं; प्रोजेक्ट पैकेज न हो, तो pythonpath तेज़ रास्ता है।
टेस्ट को क्लास में समूहित करना
tests/test_cart.py योजना की cart वाली पंक्तियों को एक क्लास में रखता है:
from shop.cart import Cart
class TestCart:
def test_empty_cart_subtotal(self):
assert Cart().subtotal() == 0
def test_subtotal_counts_quantity(self):
cart = Cart()
cart.add("pen", 15.0, quantity=3)
assert cart.subtotal() == 45.0
def test_total_adds_tax(self):
cart = Cart()
cart.add("bag", 100.0)
assert cart.total() == 115.0$ pytest -q tests/test_cart.py
... [100%]
3 passed in 0.01sकिसी base class से inherit करने की ज़रूरत नहीं — TestCart एक साधारण क्लास है, और self बस ज़रूरी पहला parameter है। तो क्लास क्यों? क्योंकि क्लास संबंधित टेस्ट को एक नाम देती है, और वह नाम चुनने लायक चीज़ बन जाता है: एक argument से "cart के सारे टेस्ट" चलाइए।
क्लास किस काम के लिए नहीं है — state साझा करने के लिए। pytest हर टेस्ट मेथड के लिए एक नया instance बनाता है, इसलिए एक टेस्ट में self पर रखी value अगले टेस्ट में नहीं रहती। यह जानबूझकर है — जो टेस्ट एक-दूसरे के बचे-खुचे पर निर्भर हों, वे इस पर पास या फ़ेल होते हैं कि किस क्रम में चले। ऊपर का हर टेस्ट अपना Cart() ख़ुद बनाता है। साझा सेटअप का सही औज़ार fixtures हैं, एक बाद के अध्याय में।
Node ID: एक टेस्ट का पता
बिना कुछ चलाए pytest से कहिए कि उसने जो collect किया उसकी सूची दे — --collect-only, या छोटे में --co:
$ pytest --co -q
tests/test_cart.py::TestCart::test_empty_cart_subtotal
tests/test_cart.py::TestCart::test_subtotal_counts_quantity
tests/test_cart.py::TestCart::test_total_adds_tax
tests/test_pricing.py::test_discount_ten_percent
tests/test_pricing.py::test_discount_zero
tests/test_pricing.py::test_add_tax_default_rate
6 tests collected in 0.01sइसे योजना के आख़िरी कॉलम से मिलाइए: हर पंक्ति मौजूद है, उसी पते पर जो आपने चुना था। हर लाइन एक node ID है — फ़ाइल का path, फिर ::, फिर क्लास अगर हो, फिर :: और टेस्ट का नाम। failure summary में FAILED के बाद यही string आती है, इसलिए आप उसे failure से कॉपी करके सीधे कमांड लाइन में चिपका सकते हैं।
क्या चलाना है, यह चुनना
Path या node ID से
एक टेस्ट चलाने का ग़लत तरीका है बाकी को comment कर देना, या उनके नाम बदल देना ताकि pytest उन्हें छोड़ दे — और फिर वापस करना भूल जाना। सही तरीका है pytest को एक पता देना:
$ pytest -q tests/test_pricing.py
... [100%]
3 passed in 0.01s
$ pytest -q tests/test_cart.py::TestCart
... [100%]
3 passed in 0.01s
$ pytest -q tests/test_cart.py::TestCart::test_total_adds_tax
. [100%]
1 passed in 0.01sएक फ़ोल्डर, एक फ़ाइल, एक क्लास, एक अकेला टेस्ट — node ID का हर स्तर चुनाव को और सीमित करता है, और कोड को छूना भी नहीं पड़ता।
नाम से: -k
-k एक expression लेता है और उन टेस्ट को रखता है जिनके नाम उससे मेल खाते हैं:
$ pytest -q -k discount
.. [100%]
2 passed, 4 deselected in 0.01sमिलान case-insensitive substring खोज है, और शब्दों को and, or, not और कोष्ठकों से जोड़ा जा सकता है। Expression को quotes में रखिए ताकि shell उसे एक ही argument की तरह भेजे:
$ pytest -q -k "discount or tax"
.... [100%]
4 passed, 2 deselected in 0.01s
$ pytest -q -k "TestCart and not tax"
.. [100%]
2 passed, 4 deselected in 0.01sइसीलिए टेस्ट के नाम सोच-समझकर चुनने चाहिए: test_discount_zero को discount से, zero से, या दोनों से ढूँढा जा सकता है। test_2 जैसे नाम को किसी से भी नहीं।
एक बारीकी हर किसी को एक बार पकड़ती है। -k टेस्ट के नाम और उसके ऊपर के नामों से भी मिलान करता है — उसकी क्लास और उसकी फ़ाइल:
$ pytest --co -q -k "not cart"
tests/test_pricing.py::test_discount_ten_percent
tests/test_pricing.py::test_discount_zero
tests/test_pricing.py::test_add_tax_default_rate
3/6 tests collected (3 deselected) in 0.01sतीनों TestCart टेस्ट में से किसी के अपने नाम में cart नहीं है, फिर भी तीनों हट गए — वे test_cart.py में रहते हैं। जब कोई -k चुनाव चौंकाए, तो चलाने से पहले --co जोड़कर देख लीजिए।
नाम के बजाय अपने लगाए लेबल से चुनना markers और -m का काम है, अध्याय बारह में।
जब कुछ फ़ेल होता है
कोड को जानबूझकर तोड़िए: add_tax का डिफ़ॉल्ट rate 0.15 से 0.18 कर दीजिए। अब दो टेस्ट फ़ेल होते हैं। (--tb=no failure रिपोर्ट छिपा देता है ताकि रन की शक्ल दिखे; वे रिपोर्ट पढ़ना आप पिछले अध्याय में सीख चुके हैं।)
$ pytest -q --tb=no
..F..F [100%]
=========================== short test summary info ============================
FAILED tests/test_cart.py::TestCart::test_total_adds_tax - assert 118.0 == 115.0
FAILED tests/test_pricing.py::test_add_tax_default_rate - assert 118.0 == 115.0
2 failed, 4 passed in 0.01s-x पहले failure पर रुक जाता है। जब एक टूटा फ़ंक्शन दस टेस्ट फ़ेल करवाता है, तो पहली रिपोर्ट ही पढ़ने लायक होती है; बाकी नौ उसकी गूँज हैं:
$ pytest -q -x --tb=no
..F
=========================== short test summary info ============================
FAILED tests/test_cart.py::TestCart::test_total_adds_tax - assert 118.0 == 115.0
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 2 passed in 0.01s--maxfail=N N failures के बाद रुकता है। -x ठीक --maxfail=1 है।
--lf (last failed) सिर्फ़ वही दोबारा चलाता है जो पिछली बार फ़ेल हुआ था। pytest failures को प्रोजेक्ट के .pytest_cache फ़ोल्डर में याद रखता है:
$ pytest -q --lf --tb=no
FF [100%]
=========================== short test summary info ============================
FAILED tests/test_cart.py::TestCart::test_total_adds_tax - assert 118.0 == 115.0
FAILED tests/test_pricing.py::test_add_tax_default_rate - assert 118.0 == 115.0
2 failed, 2 deselected in 0.01sसिर्फ़ दो फ़ेल टेस्ट चले। rate वापस कर दीजिए, तो --lf वही दो फिर चलाता है और वे पास होते हैं। कुछ भी फ़ेल न बचा हो तब एक बार और --lf चलाइए, तो pytest छापता है run-last-failure: no previously failed tests, not deselecting items. और सब कुछ चलाता है।
--ff (failed first) पहले फ़ेल हुए टेस्ट चलाता है, फिर बाकी सब। जल्दी का जवाब ऊपर आता है, पूरी सुरक्षा जाँच उसके बाद:
$ pytest -q --ff --tb=no
FF.... [100%]
=========================== short test summary info ============================
FAILED tests/test_cart.py::TestCart::test_total_adds_tax - assert 118.0 == 115.0
FAILED tests/test_pricing.py::test_add_tax_default_rate - assert 118.0 == 115.0
2 failed, 4 passed in 0.01sFF.... — दोनों फ़ेल टेस्ट पहले चले, पास होने वाले चार से पहले।
बग ठीक करते समय एक अच्छा चक्र: हरा होने तक pytest --lf -x, फिर एक बार सिर्फ़ pytest। आख़िरी कदम क्यों? क्योंकि --lf सिर्फ़ लाल वालों को दोबारा चलाता है — वह यह नहीं बता सकता कि आपके fix ने कोई ऐसी चीज़ तोड़ दी जो पहले हरी थी।
टेस्ट का आउटपुट: capturing और -s
टेस्ट के अंदर print कुछ करता हुआ नहीं दिखता। (नीचे --tb=short बस failure रिपोर्ट को छोटा करता है।) टेस्ट स्क्रीन पर जो कुछ लिखता है, pytest उसे capture कर लेता है और सिर्फ़ तभी दिखाता है जब वह टेस्ट फ़ेल हो:
def test_quiet():
print("subtotal is", 45.0)
assert 45.0 == 45.0
def test_loud():
print("subtotal is", 40.0)
assert 40.0 == 45.0$ pytest -q --tb=short
.F [100%]
=================================== FAILURES ===================================
__________________________________ test_loud ___________________________________
test_print.py:8: in test_loud
assert 40.0 == 45.0
E assert 40.0 == 45.0
----------------------------- Captured stdout call -----------------------------
subtotal is 40.0
=========================== short test summary info ============================
FAILED test_print.py::test_loud - assert 40.0 == 45.0
1 failed, 1 passed in 0.01sपास होने वाले टेस्ट का आउटपुट ग़ायब हो गया; फ़ेल होने वाले का आउटपुट उसकी रिपोर्ट में Captured stdout call के नीचे जुड़ा है। सैकड़ों टेस्ट के साथ आप यही चाहते हैं — सब ठीक हो तो ख़ामोशी, न हो तो सबूत। सब कुछ होते-होते देखना हो, तो -s capturing बंद कर देता है:
$ pytest -q -s --tb=no
subtotal is 45.0
.subtotal is 40.0
F
=========================== short test summary info ============================
FAILED test_print.py::test_loud - assert 40.0 == 45.0
1 failed, 1 passed in 0.01sExit codes
काम ख़त्म करके pytest shell को एक संख्या देता है। CI सिस्टम सिर्फ़ उसी संख्या से तय करते हैं कि "हरा" है या "लाल":
| कोड | अर्थ | |---|---| | 0 | collect हुए सारे टेस्ट पास हुए | | 1 | टेस्ट चले, और कम से कम एक फ़ेल हुआ | | 2 | रन बीच में रुका — collection के दौरान error, या Ctrl+C | | 3 | pytest या किसी plugin के अंदर की internal error | | 4 | उपयोग की ग़लती — अनजान option, या ऐसा path या node ID जो मौजूद नहीं | | 5 | कोई टेस्ट collect नहीं हुआ |
5 पर ध्यान देना ज़रूरी है। जो -k किसी से मेल न खाए, या जिस फ़ोल्डर में सही नाम वाली कोई फ़ाइल न हो, वह पास नहीं है — और CI इसे failure मानता है, जो बिल्कुल सही है:
$ pytest -q -k refund
6 deselected in 0.01s
$ echo $?
5अध्याय की शुरुआत वाला ModuleNotFoundError Interrupted: 1 error during collection पर ख़त्म हुआ था — exit code 2। Collection error कोई फ़ेल टेस्ट नहीं है; pytest टेस्ट चलाने तक पहुँचा ही नहीं।
पूर्ण उदाहरण
योजना बन चुकी है और हरी है। अब tests/test_cart.py में एक धीमा टेस्ट जुड़ता है, अपनी अलग क्लास में:
from shop.cart import Cart
class TestBigCart:
def test_many_items(self):
cart = Cart()
for _ in range(300_000):
cart.add("pen", 1.0)
assert cart.subtotal() == 300_000.0फ़ाइल में इस क्लास के ऊपर TestCart अब भी है। पहले, कुछ चलाने से पहले, देखिए कि pytest क्या देखता है:
$ pytest --co -q
tests/test_cart.py::TestCart::test_empty_cart_subtotal
tests/test_cart.py::TestCart::test_subtotal_counts_quantity
tests/test_cart.py::TestCart::test_total_adds_tax
tests/test_cart.py::TestBigCart::test_many_items
tests/test_pricing.py::test_discount_ten_percent
tests/test_pricing.py::test_discount_zero
tests/test_pricing.py::test_add_tax_default_rate
7 tests collected in 0.01sसात टेस्ट, हर एक का एक पता। सबको चलाइए और सबसे धीमे तीन माँगिए:
$ pytest -q --durations=3
....... [100%]
============================= slowest 3 durations ==============================
0.04s call tests/test_cart.py::TestBigCart::test_many_items
(2 durations < 0.005s hidden. Use -vv to show these durations.)
7 passed in 0.12sसूची में आने लायक धीमा सिर्फ़ एक टेस्ट था। असली प्रोजेक्ट पर --durations=10 से ही वे गिने-चुने टेस्ट मिलते हैं जिनकी वजह से पूरा suite धीमा लगता है। cart पर काम करते समय धीमे वाले को बाहर रखिए:
$ pytest -q -k "cart and not many"
... [100%]
3 passed, 4 deselected in 0.01sतीन चीज़ों ने इसे संभव बनाया। pythonpath = ["src"] की वजह से from shop.cart import Cart सफल होता ही है। TestBigCart collect होता है क्योंकि उसका नाम Test से शुरू होता है और उसमें __init__ नहीं है। और -k "cart and not many" ने ठीक TestCart के टेस्ट रखे: फ़ाइल के नाम के ज़रिए cart ने test_cart.py की हर चीज़ से मेल खाया, और not many ने धीमे वाले को हटा दिया।
जब यह काम न करे
Collection के दौरान ModuleNotFoundError: No module named 'shop' टेस्ट किया जाने वाला कोड sys.path पर नहीं है। src/ layout में [tool.pytest.ini_options] के नीचे pythonpath = ["src"] जोड़िए, या pip install -e . से पैकेज इंस्टॉल कीजिए। रन Interrupted: 1 error during collection और exit code 2 पर ख़त्म होता है।
python -m pytest से चलता है, पर pytest से नहीं python -m pytest मौजूदा फ़ोल्डर को भी sys.path पर रखता है; सिर्फ़ pytest नहीं रखता। इसलिए प्रोजेक्ट root में रखा मॉड्यूल एक तरीके से import होता है और दूसरे से नहीं। इस अंतर पर भरोसा मत कीजिए — pythonpath सेट कीजिए, या पैकेज इंस्टॉल कीजिए।
PytestCollectionWarning: cannot collect test class 'TestCart' because it has a __init__ constructor टेस्ट क्लास में __init__ नहीं होना चाहिए। उसे हटाइए और हर टेस्ट की ज़रूरत की चीज़ टेस्ट के अंदर ही बनाइए।
no tests ran, exit code 5 Discovery के नियमों से कुछ भी मेल नहीं खाया। फ़ाइल का नाम (test_*.py या *_test.py), फ़ंक्शनों के नाम (test...), क्लासों के नाम (Test...) जाँचिए। अगर -k इस्तेमाल किया, तो शायद वह किसी से मेल नहीं खाता।
ERROR: not found: .../tests/test_cart.py::test_total_adds_tax Node ID ग़लत है। यह टेस्ट TestCart के अंदर रहता है, इसलिए इसका पता tests/test_cart.py::TestCart::test_total_adds_tax है। Node ID टाइप करने के बजाय pytest --co -q से कॉपी कीजिए। Exit code 4।
ERROR: file or directory not found: tests/test_carts.py Path में टाइपो। Exit code 4।
pytest: error: unrecognized arguments: --maxfial=2 Option की स्पेलिंग ग़लत; असली options की सूची pytest --help देता है। Exit code 4।
चरण 4 / 6 — अनुमान
अपनी समझ की जाँच करें
यह फ़ाइल test_rules.py नाम से सेव है। pytest -q चलाने पर आख़िरी लाइन कितने पास बताएगी?
def test_found():
assert True
def check_total():
assert True
class TestGroup:
def test_method(self):
assert True
def helper(self):
assert True
class CartTests:
def test_ignored(self):
assert True- A3 passed
- B4 passed
- C2 passed
- D1 passed
कोड src/shop/pricing.py में है। pyproject.toml में [tool.pytest.ini_options] के नीचे pythonpath = ["shop"] लिखा है। फिर भी pytest कहता है ModuleNotFoundError: No module named 'shop'। ग़लती कहाँ है?
# tests/test_pricing.py
from shop.pricing import add_tax
def test_add_tax_default_rate():
assert add_tax(100.0) == 115.0- Aटेस्ट फ़ाइल का नाम `_test.py` पर ख़त्म होना चाहिए
- B`pythonpath` को उस फ़ोल्डर की ओर इशारा करना चाहिए जिसके *अंदर* `shop` पैकेज है: `["src"]`
- Cफ़ंक्शन का नाम `Test` से शुरू होना चाहिए
- Dकुछ भी import होने से पहले `tests/` में `__init__.py` चाहिए
दो टेस्ट फ़ेल हो रहे थे और आपने बग ठीक कर दिया। कौन-सी कमांड सिर्फ़ उन्हीं दो टेस्ट को दोबारा चलाएगी?
- Apytest --ff
- Bpytest -x
- Cpytest --co
- Dpytest --lf
उत्तर देने के लिए अकाउंट आवश्यक है
अपने उत्तर जाँचने के लिए साइन इन करें
प्रश्न ऊपर दिए गए हैं, और मन में उत्तर सोचना ही मुख्य कार्य है। सही उत्तर, व्याख्या और तीन-स्तरीय संकेत देखने के लिए साइन इन करें।
आपकी बारी
एक लाइब्रेरी देर से लौटाई गई किताबों पर जुर्माना लेती है। यह प्रोजेक्ट बनाइए:
library/
├── src/
│ └── library/
│ ├── __init__.py
│ └── fees.py
└── tests/
└── test_fees.pysrc/library/fees.py:
def late_fee(days_late, daily_rate=5):
if days_late < 0:
raise ValueError(f"days_late cannot be negative: {days_late}")
return min(days_late * daily_rate, 100)- पहले एक टेस्ट योजना लिखिए: समय पर, कुछ दिन देर, ठीक अधिकतम सीमा (cap) पर, cap से आगे, अपनी दैनिक दर।
- उन केसों को एक क्लास
TestLateFeeके मेथड के रूप मेंtests/test_fees.pyमें लिखिए।pyproject.tomlके बिनाpytestचलाइए, error पढ़िए, औरecho $?देखिए। pyproject.tomlजोड़िए ताकि टेस्टlibraryimport कर सकें। हरा होने तक चलाइए।--co -qसे node ID की सूची देखिए। सिर्फ़ "cap से आगे" वाला टेस्ट उसके node ID से चलाइए।-kसे सिर्फ़ cap वाले टेस्ट चलाइए, फिर उन्हें छोड़कर बाकी सब।fees.pyमें cap100से90कर दीजिए। suite चलाइए, फिर--lf। वापस ठीक करके--lfचलाइए, फिर सब कुछ।
समाधान
योजना:
| केस | इनपुट | अपेक्षित | |---|---|---| | समय पर | late_fee(0) | 0 | | कुछ दिन देर | late_fee(3) | 15 | | सीमा: ठीक cap पर | late_fee(20) | 100 | | cap से आगे | late_fee(30) | 100 | | अपनी दर | late_fee(3, daily_rate=2) | 6 | | ऋणात्मक दिन | late_fee(-1) | ValueError — बाद में, pytest.raises से |
tests/test_fees.py:
from library.fees import late_fee
class TestLateFee:
def test_on_time_costs_nothing(self):
assert late_fee(0) == 0
def test_three_days_late(self):
assert late_fee(3) == 15
def test_fee_reaches_cap_exactly(self):
assert late_fee(20) == 100
def test_fee_never_exceeds_cap(self):
assert late_fee(30) == 100
def test_custom_daily_rate(self):
assert late_fee(3, daily_rate=2) == 6बिना configuration के रन collection पर ही ModuleNotFoundError: No module named 'library' और Interrupted: 1 error during collection के साथ रुक जाता है, और echo $? 2 छापता है। फिर pyproject.toml:
[tool.pytest.ini_options]
pythonpath = ["src"]
testpaths = ["tests"]$ pytest -q
..... [100%]
5 passed in 0.01s
$ pytest --co -q
tests/test_fees.py::TestLateFee::test_on_time_costs_nothing
tests/test_fees.py::TestLateFee::test_three_days_late
tests/test_fees.py::TestLateFee::test_fee_reaches_cap_exactly
tests/test_fees.py::TestLateFee::test_fee_never_exceeds_cap
tests/test_fees.py::TestLateFee::test_custom_daily_rate
5 tests collected in 0.01s
$ pytest -q tests/test_fees.py::TestLateFee::test_fee_never_exceeds_cap
. [100%]
1 passed in 0.01s
$ pytest -q -k cap
.. [100%]
2 passed, 3 deselected in 0.01s
$ pytest -q -k "not cap"
... [100%]
3 passed, 2 deselected in 0.01scap को 90 करने के बाद:
$ pytest -q --tb=no
..FF. [100%]
=========================== short test summary info ============================
FAILED tests/test_fees.py::TestLateFee::test_fee_reaches_cap_exactly - assert...
FAILED tests/test_fees.py::TestLateFee::test_fee_never_exceeds_cap - assert 9...
2 failed, 3 passed in 0.01s
$ pytest -q --lf --tb=no
FF [100%]
=========================== short test summary info ============================
FAILED tests/test_fees.py::TestLateFee::test_fee_reaches_cap_exactly - assert...
FAILED tests/test_fees.py::TestLateFee::test_fee_never_exceeds_cap - assert 9...
2 failed, 3 deselected in 0.01scap वापस 100 करने पर pytest -q --lf देता है 2 passed, 3 deselected, और पूरा pytest -q देता है 5 passed।
इन फ़ैसलों की वजह:
- cap के लिए एक नहीं, दो टेस्ट।
late_fee(20)ठीक सीमा पर है — वह पहला दिन जब जुर्माना cap तक पहुँचता है;late_fee(30)उससे काफ़ी आगे है। Off-by-one ग़लतियाँ सीमाओं पर ही रहती हैं, इसलिए सीमा को अपना टेस्ट मिलता है — और दूसरा टेस्ट साबित करता है कि cap उसके आगे भी टिका रहता है। - दोनों cap टेस्ट के नाम में
capशब्द है, और सिर्फ़ वहीं। इसी से-k capऔर-k "not cap"साफ़-साफ़ चुन पाए — नाम चुनाव को ध्यान में रखकर रखे गए थे। - एक क्लास, कोई
__init__नहीं, कोई साझा state नहीं। हर मेथडlate_feeको ख़ुद बुलाता है; क्लास सिर्फ़ समूह को एक पता देती है,tests/test_fees.py::TestLateFee। - टेस्ट फ़ाइल में
sys.pathवाली लाइन के बजायpythonpath, ताकि टेस्ट किसी भी फ़ोल्डर से चलाने पर पास हों। --lfके बाद पूरा रन।--lfसाबित करता है कि दोनों लाल टेस्ट फिर हरे हैं; बाकी तीन अब भी हरे हैं, यह सिर्फ़ पूरा रन साबित करता है।
Step 6 of 6
चुनौती — the chapter quiz
सरल से कठिन — दस प्रश्न, अंतिम वाले जानबूझकर चुनौतीपूर्ण बनाए गए हैं।
Sign in to take the quiz