अध्याय 02

assert और फ़ेल्योर रिपोर्ट पढ़ना

pytest एक साधारण assert के अंदर की वैल्यूज़ कैसे दिखाता है, फ़ेल्योर रिपोर्ट को लाइन-दर-लाइन कैसे पढ़ें, और कौन-सी तुलना लिखें ताकि रिपोर्ट सबसे ज़्यादा बताए।

45 मिनटPython 3.12
  1. 1समस्या
  2. 2समझें
  3. 3उदाहरण
  4. 4अनुमान
  5. 5स्वयं करें
  6. 6चुनौती

वह समस्या जिसे हम हल कर रहे हैं

यह रही सादे पायथन में लिखी एक जाँच, check.py नाम की फ़ाइल में:

python
def full_name(first, last):
    return f"{first} {last}".title()


assert full_name("ada", "lovelace") == "Ada Lovelace "
text
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 चलाएँ:

python
from names import full_name


def test_full_name():
    assert full_name("ada", "lovelace") == "Ada Lovelace "
text
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:

text
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:

python
def subtotal(items):
    return sum(price * qty for price, qty in items)


def total(items, discount):
    return subtotal(items) - discount

और test_cart.py, जिसमें दूसरा test गलत संख्या की अपेक्षा करता है:

python
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 चलाएँ:

text
============================= 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 से nested where लाइनें बनती हैं, हर एक थोड़ा और अंदर खिसकी हुई।
  • 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:

python
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, चार तरह की तुलना:

python
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 रन से काटकर:

text
>       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 में है?" पूछने का गलत तरीका:

python
def test_count():
    emails = ["ada@example.com"]
    assert emails.count("alan@example.com") > 0
text
>       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 लौटाता है:

python
def overdue(invoices):
    return [i for i in invoices if i["days"] > 30]


def is_adult(age):
    if age >= 18:
        return "yes"
    return "no"
python
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 True

pytest -q, तीनों failures को उनकी E लाइनों तक छोटा करके:

text
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:

python
def in_stock(qty):
    return qty and qty > 0
python
from 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
text
.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 दोनों तरफ़ की तुलना करता है और सिर्फ़ वही दिखाता है जो अलग है।

python
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 लाइनें:

text
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 के साथ भी यही होता है, और एक जैसा हिस्सा छोड़ दिया जाता है:

python
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"
text
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 के साथ:

text
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 अपनी व्याख्या के ऊपर प्रिंट करता है:

python
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"
text
>           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) में लपेटने का मन करता है:

python
def test_total():
    total = 80.0
    assert (total == 70.0, "total is wrong")
text
.                                                                        [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.01s

Test पास हो गया, जबकि total का मान 80.0 है। अंदर कॉमा वाले कोष्ठक दो आइटम्स का एक tuple बनाते हैं, और एक ग़ैर-खाली tuple हमेशा truthy होता है — assert tuple को जाँचता है, तुलना को नहीं। pytest चेतावनी देता है, लेकिन लंबे रन के अंत में आई चेतावनी आसानी से छूट जाती है। कोष्ठक हटा दें:

python
def test_total():
    total = 80.0
    assert total == 70.0, "total is wrong"
text
>       assert total == 70.0, "total is wrong"
E       AssertionError: total is wrong
E       assert 80.0 == 70.0

अगर लाइन बहुत लंबी है, तो कोष्ठक सिर्फ़ मैसेज के आसपास लगाएँ:

python
def test_total():
    total = 80.0
    assert total == 70.0, (
        "total is wrong after applying the loyalty discount"
    )

हर व्यवहार के लिए एक assert

जो test फ़ील्ड्स को एक के बाद एक जाँचता है, वह पहले फ़ेल होने वाले पर रुक जाता है। orders.py में दो bugs हैं — नाम से स्पेस नहीं हटाए जाते और मात्रा string ही रह जाती है:

python
def parse_order(line):
    order_id, customer, qty, price = line.split(",")
    return {"id": int(order_id), "customer": customer, "qty": qty, "price": float(price)}

पहले गलत तरीका, फिर सही तरीका:

python
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}
text
>       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 के साथ:

text
___________________________ 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 के लिए एक स्थान देता है:

text
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 केवल सारांश छोड़ता है:

text
.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:

text
>       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" जैसी लाइनें पढ़ता है:

python
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 None

test_invoice.py "टेस्ट लिखने से पहले" वाली योजना है, हर पंक्ति के लिए एक test, और हर एक तालिका के आख़िरी कॉलम वाली तुलना से लिखा गया है:

python
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:

text
......                                                                   [100%]
6 passed in 0.01s

अब कोई summarise को "साफ़-सुथरा" करता है ताकि नाम क्रमबद्ध (sorted) होकर लौटें, और एक लाइन को बदलकर "names": sorted(item["name"] for item in items), कर देता है। pytest -q --tb=short:

text
..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 कभी फ़ेल नहीं हुआ, उसका काम करना कभी साबित नहीं हुआ — एक बार जानबूझकर कोड तोड़ें और उसे लाल होते देखें।