assert और फ़ेल्योर रिपोर्ट पढ़ना
pytest एक साधारण assert के अंदर की वैल्यूज़ कैसे दिखाता है, फ़ेल्योर रिपोर्ट को लाइन-दर-लाइन कैसे पढ़ें, और कौन-सी तुलना लिखें ताकि रिपोर्ट सबसे ज़्यादा बताए।
- 1समस्या
- 2समझें
- 3उदाहरण
- 4अनुमान
- 5स्वयं करें
- 6चुनौती
वह समस्या जिसे हम हल कर रहे हैं
यह रही सादे पायथन में लिखी एक जाँच, check.py नाम की फ़ाइल में:
def full_name(first, last):
return f"{first} {last}".title()
assert full_name("ada", "lovelace") == "Ada Lovelace "Traceback (most recent call last):
File "/home/you/shop/check.py", line 5, in <module>
assert full_name("ada", "lovelace") == "Ada Lovelace "
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionErrorपायथन आपको बस इतना बताता है कि दावा गलत था, और कुछ नहीं। full_name ने असल में क्या लौटाया? यह जानने के लिए आपको एक print जोड़ना होगा, उसे दोबारा चलाना होगा, और देखना होगा।
अब यही लाइन एक test में, test_names.py में रखें, और pytest -q चलाएँ:
from names import full_name
def test_full_name():
assert full_name("ada", "lovelace") == "Ada Lovelace "F [100%]
=================================== FAILURES ===================================
________________________________ test_full_name ________________________________
def test_full_name():
> assert full_name("ada", "lovelace") == "Ada Lovelace "
E AssertionError: assert 'Ada Lovelace' == 'Ada Lovelace '
E
E - Ada Lovelace
E ? -
E + Ada Lovelace
test_names.py:5: AssertionError
=========================== short test summary info ============================
FAILED test_names.py::test_full_name - AssertionError: assert 'Ada Lovelace' ...
1 failed in 0.01sअब आप दोनों वैल्यूज़ देख सकते हैं, और उस एक कैरेक्टर के नीचे एक निशान भी जो अलग है: अपेक्षित string के अंत में एक अतिरिक्त स्पेस। फ़ंक्शन सही था; test गलत था।
एक फ़ेल होता test उतना ही उपयोगी है जितना वह आपको बताता है। यह अध्याय उस सब कुछ को पढ़ने के बारे में है जो pytest आपको बताता है, और ऐसे assert लिखने के बारे में है जो उसे कहने लायक कुछ दें।
इस अध्याय के अंत में आप कर पाएंगे
- समझाना कि assertion rewriting क्या है, और यह कहाँ लागू होती है और कहाँ नहीं
- किसी failure report को लाइन-दर-लाइन पढ़ना:
>,E,where,file:lineऔर छोटा सारांश ==,in,is None,is Trueऔर एक साधारणassert xमें से सही का चुनाव करना- list, dict और string के diffs पढ़ना, और
-vvसे पूरा diff माँगना - किसी assert में एक मैसेज जोड़ना, और उस tuple से बचना जो हमेशा सच होता है
--tb=short,--tb=line,--tb=noऔर-lसे report का रूप तय करना
ज़रूरी शर्तें: pytest इंस्टॉल करना और आपका पहला test।
टेस्ट लिखने से पहले
एक assert लिखित रूप में किया गया एक वादा है। उसे टाइप करने से पहले तीन सवालों के जवाब चाहिए, और उनमें से किसी का भी pytest से कोई लेना-देना नहीं है।
ठीक-ठीक क्या वादा किया गया है? वह फ़ंक्शन लीजिए जिस पर यह अध्याय समाप्त होता है, summarise(lines), जो "pen, 12, 15.0" जैसी इनवॉइस लाइनें पढ़ता है। एक वाक्य में इसका अनुबंध: यह खाली लाइनों को छोड़ देता है और एक dict लौटाता है जिसमें आइटम्स की संख्या, दिए गए क्रम में उनके नाम, और दो दशमलव स्थानों तक राउंड किया गया कुल योग होता है। आगे आप जो भी assert लिखेंगे, वह उसी वाक्य का एक हिस्सा होगा। अगर आप वह वाक्य नहीं कह सकते, तो आप test लिखने के लिए तैयार नहीं हैं — आप बस वही test कर बैठेंगे जो कोड संयोग से करता है।
क्या पहले से तैयार होना चाहिए? इस अध्याय के लिए बहुत कम: पहले अध्याय वाला virtual environment जिसमें pytest इंस्टॉल हो (python -m pytest --version जवाब देता हो), और जिस मॉड्यूल का test हो रहा है वह उस जगह से import हो सके जहाँ से आप pytest चलाते हैं — यहाँ invoice.py, test_invoice.py के बगल में रखी है और आप उसी फ़ोल्डर से pytest चलाते हैं। कोई फ़ाइल नहीं, कोई नेटवर्क नहीं, कोई fixture नहीं।
कौन-से केस, और हर एक में क्या अपेक्षित है? अपेक्षित वैल्यूज़ हाथ से निकालें, कोड चलाने से पहले। 12 × 15.0 + 2 × 850.0 का मान 1880.0 है; यह संख्या गणित से आती है, इससे नहीं कि summarise ने क्या प्रिंट किया। अगर आप इसके बजाय आउटपुट को test में कॉपी कर लेते हैं, तो test आज के व्यवहार को दर्ज कर लेता है, bugs समेत, और उसके बाद से उस bug का बचाव करता रहेगा।
| केस | इनपुट | अपेक्षित | आप कौन-सा assert लिखेंगे | | --- | --- | --- | --- | | एक लाइन, नाम के आसपास स्पेस | " pen , 12, 15.0" | {"name": "pen", "qty": 12, "price": 15.0} | पूरे dict पर == | | खाली लाइनें छोड़ दी जाती हैं | ["pen, 12, 15.0", "", "bag, 2, 850.0"] | count 2 है | == | | पूरा सारांश | वही तीन लाइनें | {"count": 2, "names": ["pen", "bag"], "total": 1880.0} | पूरे dict पर == | | मौजूद आइटम को ढूँढना | "pen" | एक रिकॉर्ड, None नहीं | is not None | | ऐसे आइटम को ढूँढना जो मौजूद नहीं | खाली list, "pen" | None | is None | | कुल योग समझ में आता है | वही तीन लाइनें | 0 से अधिक | मैसेज के साथ > |
आख़िरी कॉलम ही इस अध्याय का विषय है: हर वादे के लिए वह तुलना जो उसे सबसे सटीक रूप से कहती है और फ़ेल होने पर सबसे उपयोगी report देती है।
क्या test नहीं करना है। यह कि str.split विभाजित करता है, round राउंड करता है, int("12") का मान 12 है — पायथन इन्हें पहले से test करता है। pytest के आउटपुट के सटीक शब्दों को test करना भी आपका काम नहीं है। "pen, twelve, 15.0" जैसी गलत लाइन योजना में ज़रूर आती है, लेकिन यह assert करने के लिए कि कोई error उठता है, pytest.raises चाहिए, जो चौथे अध्याय का विषय है; पंक्ति अभी लिख लें और test तब।
pytest एक assert के अंदर कैसे देखता है
जैसे ही कोई assert फ़ेल होता है, सादा पायथन वैल्यूज़ को फेंक देता है। pytest उन्हें सँभाल कर रखता है, क्योंकि वह आपकी test फ़ाइल को चलाने से पहले बदल देता है।
जब pytest किसी test मॉड्यूल को import करता है, तो वह हर assert स्टेटमेंट को ढूँढता है और उसे ऐसे कोड में फिर से लिखता है जो शर्त जाँचने से पहले हर बीच की वैल्यू को सहेज लेता है — full_name(...) का परिणाम, दाईं ओर की string। जब जाँच फ़ेल होती है, तो वही सहेजी गई वैल्यूज़ report बन जाती हैं। इसे assertion rewriting कहते हैं, और इसी वजह से pytest को assertEqual या assertIn की ज़रूरत नहीं पड़ती: साधारण == के साथ एक साधारण assert काफ़ी है।
यह आपके लिए क्या करता है, यह देखने के लिए इसे बंद करके देखें। उसी test पर pytest -q --assert=plain:
def test_full_name():
> assert full_name("ada", "lovelace") == "Ada Lovelace "
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E AssertionError
test_names.py:5: AssertionError
=========================== short test summary info ============================
FAILED test_names.py::test_full_name - AssertionErrorफिर से वही एक अकेला शब्द। Rewriting test फ़ाइलों (test_*.py, *_test.py) और conftest.py पर लागू होती है। यह आपके दूसरे मॉड्यूल्स पर लागू नहीं होती, और यह बात तब मायने रखती है जब आप किसी helper में assert रखते हैं — "जब यह काम न करे" में दिखाया गया है कि तब क्या होता है।
Failure report को लाइन-दर-लाइन पढ़ना
एक छोटा मॉड्यूल, cart.py:
def subtotal(items):
return sum(price * qty for price, qty in items)
def total(items, discount):
return subtotal(items) - discountऔर test_cart.py, जिसमें दूसरा test गलत संख्या की अपेक्षा करता है:
from cart import subtotal, total
def test_subtotal():
assert subtotal([(15.0, 2), (60.0, 1)]) == 90.0
def test_total_with_discount():
items = [(15.0, 2), (60.0, 1)]
assert total(items, 10) == 70.0सादा pytest चलाएँ:
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0
rootdir: /home/you/shop
collected 2 items
test_cart.py .F [100%]
=================================== FAILURES ===================================
___________________________ test_total_with_discount ___________________________
def test_total_with_discount():
items = [(15.0, 2), (60.0, 1)]
> assert total(items, 10) == 70.0
E assert 80.0 == 70.0
E + where 80.0 = total([(15.0, 2), (60.0, 1)], 10)
test_cart.py:10: AssertionError
=========================== short test summary info ============================
FAILED test_cart.py::test_total_with_discount - assert 80.0 == 70.0
========================= 1 failed, 1 passed in 0.01s ==========================इसे ऊपर से पढ़ें:
test_cart.py .F— हर test के लिए एक कैरेक्टर, क्रम से।.यानी पास हुआ,Fयानी फ़ेल हुआ।____ test_total_with_discount ____— एक failure का शीर्षक। हर फ़ेल होने वाले test को अपना अलग हिस्सा मिलता है।- सोर्स की लाइनें — test का body, उस लाइन तक जो फ़ेल हुई।
>— वह सटीक लाइन जिसने exception उठाया। उसके ऊपर का सब कुछ बिना किसी शिकायत के चला।E assert 80.0 == 70.0— वही assert दोबारा, जिसमें हर expression की जगह उसकी वैल्यू है। बाईं ओर80.0था, दाईं ओर70.0।E + where 80.0 = total(...)— कोई वैल्यू कहाँ से आई। किसी function call के लिए pytest उस call को उसके arguments भरकर दिखाता है। Nested calls से nestedwhereलाइनें बनती हैं, हर एक थोड़ा और अंदर खिसकी हुई।test_cart.py:10: AssertionError— फ़ाइल और लाइन, जिस पर आप अपने एडिटर में सीधे जा सकते हैं, और exception का प्रकार।short test summary info— हर failure के लिए एक लाइन,FAILED path::test_name - error की पहली लाइन। जब failures बहुत हों, तो यह सूची सबसे पहले पढ़ें।- आख़िरी लाइन — गिनतियाँ और समय।
इसी क्रम में क्यों पढ़ें? क्योंकि हर लाइन खोज के दायरे को छोटा करती है। सारांश बताता है कौन-से tests; > लाइन बताती है कौन-सा दावा; E लाइन बताती है कौन-सी तरफ़ गलत है; where लाइन बताती है किस call ने उसे बनाया। उसके बाद ही आप कोड खोलते हैं — और सही फ़ंक्शन पर खोलते हैं।
कभी-कभी पहली E लाइन AssertionError: assert ... पढ़ती है और कभी सिर्फ़ assert ...। यह एक ही failure है; जब वैल्यूज़ में quote कैरेक्टर होते हैं, तो यह prefix बना रहता है।
वे तुलनाएँ जो आप सबसे ज़्यादा लिखेंगे
assert के बाद कोई भी expression आ सकता है, और pytest आम expressions को समझाता है। users.py:
def find_user(users, email):
for user in users:
if user["email"] == email:
return user
return None
def active_emails(users):
return [u["email"] for u in users if u["active"]]चार tests, चार तरह की तुलना:
from users import active_emails, find_user
ADA = {"email": "ada@example.com", "active": True}
ALAN = {"email": "alan@example.com", "active": False}
def test_equal():
assert active_emails([ADA, ALAN]) == ["ada@example.com", "alan@example.com"]
def test_in():
assert "alan@example.com" in active_emails([ADA, ALAN])
def test_is_not_none():
assert find_user([ADA], "ADA@example.com") is not None
def test_greater():
assert len(active_emails([ADA, ALAN])) > 1चारों फ़ेल होते हैं। हर एक की > और E लाइनें, एक ही pytest -q रन से काटकर:
> assert active_emails([ADA, ALAN]) == ["ada@example.com", "alan@example.com"]
E AssertionError: assert ['ada@example.com'] == ['ada@example...@example.com']
E
E Right contains one more item: 'alan@example.com'
E Use -v to get more diff
> assert "alan@example.com" in active_emails([ADA, ALAN])
E AssertionError: assert 'alan@example.com' in ['ada@example.com']
E + where ['ada@example.com'] = active_emails([{'email': 'ada@example.com', 'active': True}, {'email': 'alan@example.com', 'active': False}])
> assert find_user([ADA], "ADA@example.com") is not None
E AssertionError: assert None is not None
E + where None = find_user([{'email': 'ada@example.com', 'active': True}], 'ADA@example.com')
> assert len(active_emails([ADA, ALAN])) > 1
E AssertionError: assert 1 > 1
E + where 1 = len(['ada@example.com'])
E + where ['ada@example.com'] = active_emails([{'email': 'ada@example.com', 'active': True}, {'email': 'alan@example.com', 'active': False}])हर एक कुछ अलग कहता है:
==वैल्यूज़ की तुलना करता है। lists के लिए pytest यह भी बताता है कि वे कैसे अलग हैं — दाईं ओर एक आइटम ज़्यादा है। (active_emailsका Alan को बाहर रखना सही है; इस test की अपेक्षा गलत थी।)inसदस्यता जाँचता है, और पूरा container दिखाता है ताकि आप देख सकें कि उसकी जगह वहाँ क्या था।is None/is not Noneउस एकमात्रNoneऑब्जेक्ट के साथ पहचान (identity) जाँचता है।find_userनेNoneलौटाया क्योंकि खोज case-sensitive है।<,>,<=,>=वैसे ही काम करते हैं जैसे लिखे गए हैं। दोwhereलाइनें:1आयाlen(...)से, और वह list आईactive_emails(...)से।
अगर हर रूप को फ़ेल करवाया जा सकता है, तो चुनाव क्यों मायने रखता है? क्योंकि report उसी expression से बनती है जो आपने लिखा। "क्या Alan list में है?" पूछने का गलत तरीका:
def test_count():
emails = ["ada@example.com"]
assert emails.count("alan@example.com") > 0> assert emails.count("alan@example.com") > 0
E AssertionError: assert 0 > 0
E + where 0 = <built-in method count of list object at 0x7d9189cec640>('alan@example.com')
E + where <built-in method count of list object at 0x7d9189cec640> = ['ada@example.com'].countमुख्य लाइन assert 0 > 0 है, और जिस list की आपको परवाह है वह दूसरी where के अंत में दबी हुई है। सही तरीका, assert "alan@example.com" in emails, list को मुख्य लाइन में ले आता है। assert को उसी वाक्य की तरह लिखें जो आपके मन में था, और pytest failure को उन्हीं शब्दों में समझाएगा।
Truthiness: assert x और assert x == True एक नहीं हैं
assert x तब पास होता है जब भी x truthy हो — यानी False, None, 0, "" और खाली containers के अलावा कुछ भी। अक्सर ठीक यही आप चाहते हैं; कभी-कभी यह एक bug को छिपा देता है।
rules.py में दो फ़ंक्शन हैं। overdue सही है; is_adult में एक bug है — यह booleans की जगह strings लौटाता है:
def overdue(invoices):
return [i for i in invoices if i["days"] > 30]
def is_adult(age):
if age >= 18:
return "yes"
return "no"from rules import is_adult, overdue
def test_finds_overdue():
assert overdue([{"id": 1, "days": 12}, {"id": 2, "days": 30}])
def test_child_is_not_adult():
assert not is_adult(12)
def test_adult_truthy():
assert is_adult(40)
def test_adult_is_true():
assert is_adult(40) is Truepytest -q, तीनों failures को उनकी E लाइनों तक छोटा करके:
FF.F [100%]
E AssertionError: assert []
E + where [] = overdue([{'id': 1, 'days': 12}, {'id': 2, 'days': 30}])
E AssertionError: assert not 'no'
E + where 'no' = is_adult(12)
E AssertionError: assert 'yes' is True
E + where 'yes' = is_adult(40)
=========================== short test summary info ============================
FAILED test_rules.py::test_finds_overdue - AssertionError: assert []
FAILED test_rules.py::test_child_is_not_adult - AssertionError: assert not 'no'
FAILED test_rules.py::test_adult_is_true - AssertionError: assert 'yes' is True
3 failed, 1 passed in 0.01sपहला failure: एक साधारण assert some_list को भी उपयोगी report मिलती है। assert [] बताता है कि list खाली लौटी, गलत नहीं। तीस दिन तीस से ज़्यादा नहीं होते, इसलिए कोड सही है और test का डेटा गलत था — यह एक ही लाइन से पता चल गया।
अब FF.F में उस बिंदु को देखें। test_adult_truthy पास हो गया। is_adult string "yes" लौटाता है, जो truthy है, इसलिए एक bug वाले फ़ंक्शन ने assert is_adult(40) को संतुष्ट कर दिया। "no" भी truthy है, इसीलिए assert not is_adult(12) ने इसे पकड़ लिया — लेकिन सिर्फ़ संयोग से, इस पर निर्भर करते हुए कि आपने कौन-सा केस test किया।
तो क्या assert x == True लिखें? यह "yes" को पकड़ लेता है, लेकिन इसमें अपनी एक कमी है: पायथन में 1 == True और 0 == False होते हैं। stock.py:
def in_stock(qty):
return qty and qty > 0from stock import in_stock
def test_zero_equals_false():
assert in_stock(0) == False
def test_zero_is_false():
assert in_stock(0) is False.F [100%]
> assert in_stock(0) is False
E assert 0 is False
E + where 0 = in_stock(0)
1 failed, 1 passed in 0.01sजब qty का मान 0 होता है, तो qty and qty > 0 False नहीं बल्कि 0 लौटाता है। == वाला test पास हो गया; is वाले test ने इसे पकड़ लिया। इससे निकलने वाला नियम:
assert x/assert not xजब कोई भी truthy या falsy वैल्यू स्वीकार्य हो — "list खाली नहीं है", "कोई error मैसेज नहीं है"।assert x is True/assert x is Falseजब फ़ंक्शन एक असलीboolका वादा करता हो।assert x == Trueलगभग कभी नहीं। यहis Trueसे ढीला है और एक साधारणassert xसे कम कहता है; linters इसे चिह्नित करते हैं।
जब वैल्यूज़ बड़ी हों: diffs
छोटी वैल्यूज़ के लिए assert 80.0 == 70.0 ही पूरी कहानी है। चालीस आइटम्स की list या तीन लाइनों के ईमेल के लिए pytest दोनों तरफ़ की तुलना करता है और सिर्फ़ वही दिखाता है जो अलग है।
def test_list():
got = ["pen", "notebook", "bag", "eraser"]
assert got == ["pen", "notebook", "ruler", "eraser"]
def test_dict():
got = {"id": 7, "name": "Ada", "role": "admin", "active": True}
assert got == {"id": 7, "name": "Ada", "role": "editor", "active": True}
def test_multiline_string():
got = "Dear Ada,\nYour order 7 has shipped.\nThanks!"
assert got == "Dear Ada,\nYour order 7 has been shipped.\nThanks!"तीनों failures की E लाइनें:
E AssertionError: assert ['pen', 'note...ag', 'eraser'] == ['pen', 'note...er', 'eraser']
E
E At index 2 diff: 'bag' != 'ruler'
E Use -v to get more diff
E AssertionError: assert {'id': 7, 'na...active': True} == {'id': 7, 'na...active': True}
E
E Omitting 3 identical items, use -vv to show
E Differing items:
E {'role': 'admin'} != {'role': 'editor'}
E Use -v to get more diff
E AssertionError: assert 'Dear Ada,\nY...ped.\nThanks!' == 'Dear Ada,\nY...ped.\nThanks!'
E
E Dear Ada,
E - Your order 7 has been shipped.
E ? -----
E + Your order 7 has shipped.
E Thanks!पहली E लाइन जगह में समाने के लिए ... से छोटी कर दी जाती है; विवरण उसके नीचे की लाइनों में होता है:
- Lists — पहला index जहाँ वे अलग होती हैं, या किस तरफ़ अतिरिक्त आइटम्स हैं।
- Dicts — वे keys जिनकी वैल्यूज़ अलग हैं। एक जैसी keys गिनी जाती हैं, प्रिंट नहीं की जातीं।
- Strings — लाइन-दर-लाइन diff। दो स्पेस से शुरू होने वाली लाइनें दोनों तरफ़ एक जैसी हैं।
+==की बाईं तरफ़ को दर्शाता है,-दाईं तरफ़ को। नीचे की?लाइन सटीक कैरेक्टर्स की ओर इशारा करती है।
तो अगर आप हमेशा assert actual == expected लिखते हैं, तो + वह है जो आपके कोड ने बनाया और - वह जो आप चाहते थे। अलग-अलग tests में क्रम बदलते रहें, तो हर बार रुककर सोचना पड़ेगा कि कौन क्या है; क्रम एक जैसा रखें, तो कभी नहीं सोचना पड़ेगा।
एक लंबी, एक-लाइन वाली string के साथ भी यही होता है, और एक जैसा हिस्सा छोड़ दिया जाता है:
def test_receipt_line():
got = "2026-10-07 | order 1042 | 3 items | paid by card | ships to Dhaka | total 2420.00"
assert got == "2026-10-07 | order 1042 | 3 items | paid by card | ships to Dhaka | total 2402.00"E AssertionError: assert '2026-10-07 |...total 2420.00' == '2026-10-07 |...total 2402.00'
E
E Skipping 66 identical leading characters in diff, use -v to show
E - | total 2402.00
E ? -
E + | total 2420.00
E ? +80 कैरेक्टर की लाइन के अंत में आपस में बदले हुए दो अंक, जो आपके लिए खोज निकाले गए।
-v और -vv: पूरा diff माँगना
संकेतों पर ध्यान दें: Use -v to get more diff, use -vv to show। डिफ़ॉल्ट रूप से pytest लंबी व्याख्याओं को छोटा कर देता है, और हर -v विस्तार का स्तर बढ़ाता है। dict वाला test pytest -vv के साथ:
E AssertionError: assert {'id': 7, 'name': 'Ada', 'role': 'admin', 'active': True} == {'id': 7, 'name': 'Ada', 'role': 'editor', 'active': True}
E
E Common items:
E {'active': True, 'id': 7, 'name': 'Ada'}
E Differing items:
E {'role': 'admin'} != {'role': 'editor'}
E
E Full diff:
E {
E 'id': 7,
E 'name': 'Ada',
E - 'role': 'editor',
E ? ^ ^^^
E + 'role': 'admin',
E ? ^ + ^
E 'active': True,
E }कुछ भी छोटा नहीं किया गया: दोनों dicts पूरे, एक जैसे आइटम्स, और हर लाइन में एक key वाला diff। तो हमेशा -vv क्यों न इस्तेमाल करें? क्योंकि दो सौ रिकॉर्ड्स की list पर पूरा diff दो सौ लाइनों का होता है, और डिफ़ॉल्ट सारांश — "at index 2" — ही वह था जिसकी आपको ज़रूरत थी। डिफ़ॉल्ट से शुरू करें; -vv केवल उस एक test के लिए जोड़ें जिसका सारांश काफ़ी नहीं है।
आपका अपना मैसेज: assert cond, "msg"
एक कॉमा के बाद, assert एक मैसेज ले सकता है, जिसे pytest अपनी व्याख्या के ऊपर प्रिंट करता है:
def test_everything_in_stock():
stock = {"pen": 12, "bag": 0}
for name, qty in stock.items():
assert qty > 0, f"{name} is out of stock"> assert qty > 0, f"{name} is out of stock"
E AssertionError: bag is out of stock
E assert 0 > 0मैसेज के बिना आपको assert 0 > 0 दिखता और आपको खुद पता लगाना पड़ता कि कौन-सा आइटम शून्य था। मैसेज तब अपनी जगह का हक़दार है जब अकेली वैल्यूज़ यह न बताएँ कि कौन-सा केस फ़ेल हुआ या नियम क्यों मौजूद है। गलत इस्तेमाल है वही दोहराना जो pytest पहले से दिखाता है: "expected 70.0 but got 80.0" assert 80.0 == 70.0 में कुछ नहीं जोड़ता।
Tuple का जाल
जब मैसेज लाइन को लंबा कर देता है, तो पूरी चीज़ को कोष्ठकों (parentheses) में लपेटने का मन करता है:
def test_total():
total = 80.0
assert (total == 70.0, "total is wrong"). [100%]
=============================== warnings summary ===============================
test_trap.py:3
/home/you/shop/test_trap.py:3: PytestAssertRewriteWarning: assertion is always true, perhaps remove parentheses?
assert (total == 70.0, "total is wrong")
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
1 passed, 1 warning in 0.01sTest पास हो गया, जबकि total का मान 80.0 है। अंदर कॉमा वाले कोष्ठक दो आइटम्स का एक tuple बनाते हैं, और एक ग़ैर-खाली tuple हमेशा truthy होता है — assert tuple को जाँचता है, तुलना को नहीं। pytest चेतावनी देता है, लेकिन लंबे रन के अंत में आई चेतावनी आसानी से छूट जाती है। कोष्ठक हटा दें:
def test_total():
total = 80.0
assert total == 70.0, "total is wrong"> assert total == 70.0, "total is wrong"
E AssertionError: total is wrong
E assert 80.0 == 70.0अगर लाइन बहुत लंबी है, तो कोष्ठक सिर्फ़ मैसेज के आसपास लगाएँ:
def test_total():
total = 80.0
assert total == 70.0, (
"total is wrong after applying the loyalty discount"
)हर व्यवहार के लिए एक assert
जो test फ़ील्ड्स को एक के बाद एक जाँचता है, वह पहले फ़ेल होने वाले पर रुक जाता है। orders.py में दो bugs हैं — नाम से स्पेस नहीं हटाए जाते और मात्रा string ही रह जाती है:
def parse_order(line):
order_id, customer, qty, price = line.split(",")
return {"id": int(order_id), "customer": customer, "qty": qty, "price": float(price)}पहले गलत तरीका, फिर सही तरीका:
from orders import parse_order
def test_parse_order_fields():
order = parse_order("1042, Ada ,3,15.0")
assert order["id"] == 1042
assert order["customer"] == "Ada"
assert order["qty"] == 3
assert order["price"] == 15.0
def test_parse_order():
order = parse_order("1042, Ada ,3,15.0")
assert order == {"id": 1042, "customer": "Ada", "qty": 3, "price": 15.0}> assert order["customer"] == "Ada"
E AssertionError: assert ' Ada ' == 'Ada'
E
E - Ada
E + Ada
E ? + +
> assert order == {"id": 1042, "customer": "Ada", "qty": 3, "price": 15.0}
E AssertionError: assert {'id': 1042, ...'price': 15.0} == {'id': 1042, ...'price': 15.0}
E
E Omitting 2 identical items, use -vv to show
E Differing items:
E {'customer': ' Ada '} != {'customer': 'Ada'}
E {'qty': '3'} != {'qty': 3}
E Use -v to get more diffपहला test एक bug की report देता है। आप उसे ठीक करते हैं, फिर से चलाते हैं, और तभी दूसरे से सामना होता है। दूसरा test पूरे परिणाम की तुलना करता है और दोनों की report एक साथ देता है।
दिशानिर्देश है हर test में एक व्यवहार, न कि हर test में एक assert। "एक लाइन को parse करने से यह रिकॉर्ड बनता है" एक व्यवहार है, इसलिए पूरी वैल्यू की एक तुलना आदर्श है। कई asserts तब ठीक हैं जब वे मिलकर एक ही व्यवहार का वर्णन करें। जिससे बचना है वह है एक ऐसा test जो parsing जाँचे, फिर खाली लाइनें छोड़ना, फिर कुल योग — तीन व्यवहार, जहाँ पहला failure बाकी दो को छिपा देता है और test का नाम यह नहीं बता सकता कि क्या टूटा।
कितनी report: --tb और -l
शीर्षक और file:line के बीच का हिस्सा traceback है, और --tb तय करता है कि उसका कितना हिस्सा दिखाया जाए। cart.py वाला test pytest -q --tb=short के साथ:
___________________________ test_total_with_discount ___________________________
test_cart.py:10: in test_total_with_discount
assert total(items, 10) == 70.0
E assert 80.0 == 70.0
E + where 80.0 = total([(15.0, 2), (60.0, 1)], 10)सिर्फ़ फ़ेल होने वाली लाइन, पूरा फ़ंक्शन नहीं। --tb=line हर failure के लिए एक स्थान देता है:
E assert 80.0 == 70.0
+ where 80.0 = total([(15.0, 2), (60.0, 1)], 10)
/home/you/shop/test_cart.py:10: assert 80.0 == 70.0और --tb=no केवल सारांश छोड़ता है:
.F [100%]
=========================== short test summary info ============================
FAILED test_cart.py::test_total_with_discount - assert 80.0 == 70.0
1 failed, 1 passed in 0.01sडिफ़ॉल्ट, --tb=auto, वही लंबा रूप है जो आप पूरे अध्याय में देखते आए हैं। किसी बड़े रन में क्या फ़ेल हुआ यह देखने के लिए --tb=no या --tb=line का उपयोग करें; फिर क्यों फ़ेल हुआ यह देखने के लिए एक test को डिफ़ॉल्ट के साथ दोबारा चलाएँ।
दूसरी दिशा में, -l (--showlocals) फ़ेल होने वाले फ़ंक्शन के local variables जोड़ देता है। pytest -q -l:
> assert total(items, 10) == 70.0
E assert 80.0 == 70.0
E + where 80.0 = total([(15.0, 2), (60.0, 1)], 10)
items = [(15.0, 2), (60.0, 1)]
test_cart.py:10: AssertionErrorजिस test में loop हो या कई बीच के variables हों, उसमें यही अतिरिक्त हिस्सा अक्सर failure को समझाता है — आप इसे अंत में समाधान में देखेंगे।
पूर्ण उदाहरण
invoice.py "pen, 12, 15.0" जैसी लाइनें पढ़ता है:
def parse_line(line):
name, qty, price = line.split(",")
return {"name": name.strip(), "qty": int(qty), "price": float(price)}
def summarise(lines):
items = [parse_line(line) for line in lines if line.strip()]
total = sum(item["qty"] * item["price"] for item in items)
return {
"count": len(items),
"names": [item["name"] for item in items],
"total": round(total, 2),
}
def find_item(items, name):
for item in items:
if item["name"] == name:
return item
return Nonetest_invoice.py "टेस्ट लिखने से पहले" वाली योजना है, हर पंक्ति के लिए एक test, और हर एक तालिका के आख़िरी कॉलम वाली तुलना से लिखा गया है:
from invoice import find_item, parse_line, summarise
LINES = ["pen, 12, 15.0", "", "bag, 2, 850.0"]
def test_parse_line_strips_and_converts():
assert parse_line(" pen , 12, 15.0") == {"name": "pen", "qty": 12, "price": 15.0}
def test_blank_lines_are_skipped():
assert summarise(LINES)["count"] == 2
def test_summary():
assert summarise(LINES) == {"count": 2, "names": ["pen", "bag"], "total": 1880.0}
def test_find_item_present():
items = [parse_line("pen, 12, 15.0")]
assert find_item(items, "pen") is not None
def test_find_item_missing():
assert find_item([], "pen") is None
def test_total_is_positive():
total = summarise(LINES)["total"]
assert total > 0, f"total should be positive, got {total}"pytest -q:
...... [100%]
6 passed in 0.01sअब कोई summarise को "साफ़-सुथरा" करता है ताकि नाम क्रमबद्ध (sorted) होकर लौटें, और एक लाइन को बदलकर "names": sorted(item["name"] for item in items), कर देता है। pytest -q --tb=short:
..F... [100%]
=================================== FAILURES ===================================
_________________________________ test_summary _________________________________
test_invoice.py:15: in test_summary
assert summarise(LINES) == {"count": 2, "names": ["pen", "bag"], "total": 1880.0}
E AssertionError: assert {'count': 2, ...otal': 1880.0} == {'count': 2, ...otal': 1880.0}
E
E Omitting 2 identical items, use -vv to show
E Differing items:
E {'names': ['bag', 'pen']} != {'names': ['pen', 'bag']}
E Use -v to get more diff
=========================== short test summary info ============================
FAILED test_invoice.py::test_summary - AssertionError: assert {'count': 2, .....
1 failed, 5 passed in 0.01sइसे उसी तरह पढ़ें जैसे इस अध्याय ने सिखाया। छह में से एक test फ़ेल हुआ, test_summary, लाइन 15 पर। पहली E लाइन छोटी की गई है, इसलिए विवरण नीचे है: दो keys मेल खाईं, और names अलग है — कोड से ['bag', 'pen'] (बाईं ओर) बनाम test में ['pen', 'bag'] (दाईं ओर)। क्रम बदला, और कुछ नहीं। यह bug है या जानबूझकर किया गया बदलाव, यह अब इनवॉइस के बारे में एक फ़ैसला है, कोड में भटकती खोज नहीं।
यह भी ध्यान दें कि कौन-से tests फ़ेल नहीं हुए। count, lookups और कुल योग अब भी पास होते हैं, इसलिए report बदलाव को एक dict की एक key तक सीमित कर देती है। यही हर test में एक व्यवहार का फ़ायदा है: पास और फ़ेल का पैटर्न अपने आप में एक निदान है।
जब यह काम न करे
किसी helper मॉड्यूल से बिना वैल्यूज़ वाला E AssertionError Rewriting केवल test फ़ाइलों और conftest.py को कवर करती है। मान लीजिए checks.py के अंदर का कोई assert एक खाली AssertionError देता है। मॉड्यूल को उसके import होने से पहले रजिस्टर करें, conftest.py में सबसे ऊपर: pytest.register_assert_rewrite("checks")। इसके बाद report हमेशा की तरह assert 0 > 0 दिखाती है।
PytestAssertRewriteWarning: assertion is always true, perhaps remove parentheses? आपने assert (condition, "message") लिखा है। यह एक tuple है, और यह test कभी फ़ेल नहीं हो सकता। बाहरी कोष्ठक हटा दें; अगर लाइन लंबी है तो सिर्फ़ मैसेज को लपेटें।
AssertionError: assert None == ... या + where None = ... जिस फ़ंक्शन को आपने call किया उसने None लौटाया। आमतौर पर उसमें कोई ऐसा रास्ता होता है जिसमें return नहीं है, या वह किसी चीज़ को उसी जगह बदलता है और कुछ नहीं लौटाता — list.sort() इसका क्लासिक उदाहरण है।
Use -v to get more diff / ...Full output truncated (8 lines hidden), use '-vv' to show यह कोई error नहीं है — pytest ने व्याख्या को छोटा कर दिया। उस एक test को -vv के साथ दोबारा चलाएँ।
एक test जो तब भी पास होता है जब वैल्यू साफ़ तौर पर गलत हो assert x देखें जहाँ x कोई ग़ैर-खाली string हो जैसे "no", assert x == True जहाँ x का मान 1 हो, या tuple का जाल। जो test कभी फ़ेल नहीं हुआ, उसका काम करना कभी साबित नहीं हुआ — एक बार जानबूझकर कोड तोड़ें और उसे लाल होते देखें।
चरण 4 / 6 — अनुमान
अपनी समझ की जाँच करें
टेस्ट फ़ेल होता है। पहली E लाइन के नीचे pytest कौन-सी व्याख्या वाली लाइन छापता है?
def test_names():
got = ["pen", "bag"]
assert got == ["pen", "bag", "ink"]- AAt index 2 diff: 'ink' != None
- BLeft contains one more item: 'ink'
- CRight contains one more item: 'ink'
- Dassert 2 == 3
total का मान 80.0 है, फिर भी यह टेस्ट पास होता है। क्यों?
def test_total():
total = 80.0
assert (total == 70.0, "total is wrong")- Apytest संदेश वाले assert को छोड़ देता है
- Bfloat के लिए `80.0 == 70.0` सही है
- Cसंदेश वाली स्ट्रिंग शर्त की जगह ले लेती है
- Dकोष्ठक दो चीज़ों का एक tuple बनाते हैं, और खाली न होने वाला tuple हमेशा सही होता है
आप हमेशा assert actual == expected लिखते हैं। स्ट्रिंग diff में + और - से शुरू होने वाली लाइनें क्या दर्शाती हैं?
- A`+` अपेक्षित वैल्यू है, `-` वह जो कोड ने दिया
- B`+` वह जो कोड ने दिया (बायाँ पक्ष), `-` अपेक्षित वैल्यू (दायाँ पक्ष)
- C`+` दोनों वैल्यूज़ में जोड़े गए अक्षर, `-` दोनों में हटाए गए अक्षर
- Dयह इस पर निर्भर है कि कौन-सी वैल्यू लंबी है
उत्तर देने के लिए अकाउंट आवश्यक है
अपने उत्तर जाँचने के लिए साइन इन करें
प्रश्न ऊपर दिए गए हैं, और मन में उत्तर सोचना ही मुख्य कार्य है। सही उत्तर, व्याख्या और तीन-स्तरीय संकेत देखने के लिए साइन इन करें।
आपकी बारी
चार फ़ंक्शन्स के साथ grades.py बनाएँ:
letter(score)— 80 से"A", 65 से"B", 50 से"C", नहीं तो"F"passed(score)— एक असलीbool, 50 सेTruereport(scores)— नामों से स्कोर्स का एक dict लेता है, नामों से letters का dict लौटाता हैbest(scores)— सबसे ज़्यादा स्कोर वाला नाम, या खाली dict के लिएNone
पहले test की योजना एक तालिका के रूप में लिखें, जैसे इस अध्याय ने किया: केस, इनपुट, अपेक्षित, और वह assert जो आप इस्तेमाल करेंगे। फिर कम से कम छह tests के साथ test_grades.py लिखें। इनमें से हर एक का कम से कम एक बार उपयोग करें: पूरे dict पर ==, in, is None, is True या is False, >= जैसी कोई तुलना, और एक मैसेज वाला assert जो फ़ेल होने वाले केस का नाम बताए।
फिर जानबूझकर चीज़ें तोड़ें और हर report पढ़ें:
passedसे bool के बजायscore >= 50 and scoreलौटवाएँ। कौन-से tests इसे पकड़ते हैं?"B"की सीमा को 65 से 66 पर ले जाएँ। कौन-से tests इसे पकड़ते हैं, कौन-से नहीं — और क्यों?- अपने किसी एक assert को उसके मैसेज समेत कोष्ठकों में लपेट दें। चेतावनी ढूँढें।
- दूसरे बदलाव को बनाए रखते हुए, फ़ाइल को
--tb=lineके साथ चलाएँ, फिर फ़ेल होने वाले test को-lके साथ।
हर बदलाव के लिए report की वह पहली लाइन लिख लें जिसने आपको बताया कि क्या गलत था।
समाधान
पहले योजना। सीमाएँ ही वह जगह हैं जहाँ grading कोड गलत होता है, इसलिए हर सीमा के दोनों तरफ़ जाँच होती है — 80 और 79, 65 और 64, 50 और 49:
| केस | इनपुट | अपेक्षित | Assert | | --- | --- | --- | --- | | हर सीमा, दोनों तरफ़ | 80, 79, 65, 64, 50, 49 | A, B, B, C, C, F | स्कोर का नाम बताने वाले मैसेज के साथ == | | passed एक असली bool है | 50, 49 | True, False | is True, is False | | पूरी report | चार छात्र | letters का पूरा dict | dict पर == | | कोई फ़ेल हुआ | वही चार | letters में "F" | in | | best सच में सबसे अच्छा है | वही चार | विजेता से ऊपर कोई स्कोर नहीं | मैसेज के साथ >= | | रैंक करने को कोई नहीं | {} | None | is None |
grades.py:
def letter(score):
if score >= 80:
return "A"
if score >= 65:
return "B"
if score >= 50:
return "C"
return "F"
def passed(score):
return score >= 50
def report(scores):
return {name: letter(score) for name, score in scores.items()}
def best(scores):
if not scores:
return None
return max(scores, key=scores.get)test_grades.py:
from grades import best, letter, passed, report
SCORES = {"ada": 91, "alan": 64, "grace": 50, "linus": 49}
def test_letter_boundaries():
cases = [(80, "A"), (79, "B"), (65, "B"), (64, "C"), (50, "C"), (49, "F")]
for score, expected in cases:
assert letter(score) == expected, f"letter({score})"
def test_passed_returns_a_real_bool():
assert passed(50) is True
assert passed(49) is False
def test_report():
assert report(SCORES) == {"ada": "A", "alan": "C", "grace": "C", "linus": "F"}
def test_a_failing_letter_appears():
assert "F" in report(SCORES).values()
def test_best_has_the_highest_score():
top = best(SCORES)
for name, score in SCORES.items():
assert SCORES[top] >= score, f"{name} scored more than {top}"
def test_best_of_nobody():
assert best({}) is Nonepytest -q:
...... [100%]
6 passed in 0.01sपहला बदलाव — passed लौटाता है score >= 50 and score:
.F.... [100%]
> assert passed(50) is True
E assert 50 is True
E + where 50 = passed(50)
FAILED test_grades.py::test_passed_returns_a_real_bool - assert 50 is True
1 failed, 5 passed in 0.01s50 truthy है, इसलिए assert passed(50) इसे जाने देता। is True ने इसे पकड़ा, और assert 50 is True उस वैल्यू का नाम बताता है जिसे bool होना चाहिए था।
दूसरा बदलाव — "B" की सीमा 66 पर, --tb=line के साथ:
F..... [100%]
=================================== FAILURES ===================================
E AssertionError: letter(65)
assert 'C' == 'B'
- B
+ C
/home/you/grades/test_grades.py:9: AssertionError: letter(65)
=========================== short test summary info ============================
FAILED test_grades.py::test_letter_boundaries - AssertionError: letter(65)
1 failed, 5 passed in 0.01sसिर्फ़ सीमा वाले test ने इसे पकड़ा। test_report फिर भी पास हुआ क्योंकि SCORES में किसी भी छात्र का स्कोर ठीक 65 नहीं है — एक test केवल उन्हीं वैल्यूज़ को कवर करता है जो आप उसमें डालते हैं, और इसीलिए योजना में हर सीमा के दोनों तरफ़ सूचीबद्ध थे। मैसेज letter(65) सबसे पहले आता है, और इसकी ज़रूरत है: loop एक ही लाइन पर छह केस चलाता है, और इसके बिना आपको पता होता कि कोई letter गलत है, लेकिन यह नहीं कि किस स्कोर के लिए। + C वह है जो कोड ने लौटाया, - B वह जो test ने अपेक्षित किया।
यही failure pytest -q -l test_grades.py::test_letter_boundaries के साथ loop के variables पर समाप्त होता है:
cases = [(80, 'A'), (79, 'B'), (65, 'B'), (64, 'C'), (50, 'C'), (49, 'F')]
expected = 'B'
score = 65तीसरा बदलाव — लाइन 32 पर assert (best({}) is None, "no scores, no best"):
...... [100%]
/home/you/grades/test_grades.py:32: PytestAssertRewriteWarning: assertion is always true, perhaps remove parentheses?
6 passed, 1 warning in 0.01sछह पास हुए — और उनमें से एक अब कभी फ़ेल नहीं हो सकता, चाहे best कुछ भी लौटाए। चेतावनी ही एकमात्र संकेत है।
वे पहली लाइनें जिन्होंने आपको बताया कि क्या गलत था: assert 50 is True, AssertionError: letter(65), और assertion is always true। अब से हर failure में आप सबसे पहले ऐसी ही लाइनें खोजेंगे।
Step 6 of 6
चुनौती — the chapter quiz
सरल से कठिन — दस प्रश्न, अंतिम वाले जानबूझकर चुनौतीपूर्ण बनाए गए हैं।
Sign in to take the quiz