टेस्ट लिखने से पहले — असल में क्या चाहिए
टेस्ट कोड से पहले की सोच: अस्पष्ट requirement को सटीक contract में बदलना, कोड को टेस्ट करने लायक बनाना, boundaries के साथ केस चुनना, कोड चलाए बिना अपेक्षित जवाब जानना, और तय करना कि क्या टेस्ट नहीं करना — सब सादे पायथन और assert से।
- 1समस्या
- 2समझें
- 3उदाहरण
- 4अनुमान
- 5स्वयं करें
- 6चुनौती
वह समस्या जिसे हम हल कर रहे हैं
यह रहा एक फ़ंक्शन जो परीक्षा के अंकों की सूची का औसत निकालता है, और वह तरीका जिससे हम में से ज़्यादातर लोग ऐसे फ़ंक्शन को जाँचते हैं — उसे एक-दो बार चलाओ और देखो:
def average(scores):
return sum(scores) / len(scores)
print(average([80, 90, 70]))
print(average([100]))80.0
100.0दोनों जवाब सही हैं। फ़ंक्शन "काम करता है", और प्रोग्राम में चला जाता है। एक हफ़्ते बाद एक ऐसी क्लास, जिसके अभी कोई नतीजे नहीं आए, उसी लाइन तक पहुँचती है:
def average(scores):
return sum(scores) / len(scores)
print(average([]))Traceback (most recent call last):
File "/home/you/school/report.py", line 5, in <module>
print(average([]))
^^^^^^^^^^^
File "/home/you/school/report.py", line 2, in average
return sum(scores) / len(scores)
~~~~~~~~~~~~^~~~~~~~~~~~~
ZeroDivisionError: division by zero"जब मैंने चलाकर देखा तब तो काम कर रहा था" — यह बात सच थी। बस इसका मतलब हमेशा इतना ही था कि "यह उन दो इनपुट के लिए काम करता है जो मैंने संयोग से आज़माए"। यह घटना मैन्युअल जाँच की दो कमज़ोरियाँ उजागर करती है।
पहली यह कि यह सिर्फ़ उतना ही कवर करती है जितना आपको उस पल सूझा, और खाली सूची ठीक वैसी चीज़ है जो उस पल किसी को नहीं सूझती।
दूसरी यह कि यह दोहराई नहीं जाती। दोनों print एक बार चलाए गए, एक बार पढ़े गए, और मिटा दिए गए। अगले महीने जब फ़ंक्शन बदलेगा, तब उन्हें दोबारा कोई नहीं चलाएगा।
ऑटोमेटेड टेस्ट दूसरी कमज़ोरी को ठीक करते हैं, और इस कोर्स का बाकी हिस्सा pytest के साथ उन्हें लिखने के बारे में है। लेकिन पहली कमज़ोरी को ध्यान से देखिए, क्योंकि कोई टूल उसे ठीक नहीं करता। मान लीजिए आप अभी खाली सूची के लिए एक जाँच लिखना चाहते हैं। वह क्या कहेगी? क्या average([]) को 0 लौटाना चाहिए? None लौटाना चाहिए? एरर raise करनी चाहिए? किसी ने तय ही नहीं किया। जिस व्यवहार को किसी ने तय नहीं किया, उसका टेस्ट आप नहीं लिख सकते, और जो जवाब आपको पहले से पता नहीं, उसे आप जाँच नहीं सकते।
यही इस अध्याय का विषय है: वह सोच जो टेस्ट कोड की पहली लाइन से पहले होनी चाहिए। इसे सही कर लीजिए तो टेस्ट लगभग अपने आप लिख जाते हैं। इसे छोड़ दीजिए तो आपके पास ऐसे टेस्ट होंगे जो पास होते हैं और फिर भी बग चूक जाते हैं।
इस अध्याय के अंत में आप कर पाएंगे
- एक वाक्य में बताना कि टेस्ट क्या है: व्यवहार के बारे में एक ऐसा दावा जिसे चलाया जा सके
- एक धुँधली ज़रूरत (requirement) को एक सटीक contract में बदलना — इनपुट, आउटपुट, एरर, side effects
- ऐसे कोड को पहचानना जिसे टेस्ट करना मुश्किल है, और उसे एक pure core और पतले shell में ढालना
- equivalence classes और boundary values से केसों की योजना एक टेबल के रूप में बनाना
- अपेक्षित जवाब कोड से स्वतंत्र रूप से निकालना — यानी oracle
- तय करना कि क्या टेस्ट नहीं करना है, और कब टेस्ट काफ़ी हो गए
- योजना को सादे
assertजाँचों के रूप में लिखना, उन्हें चलाना, और बताना कि वे अभी भी काफ़ी क्यों नहीं हैं
ज़रूरी शर्तें: कोई नहीं — कोर्स यहीं से शुरू होता है। आपको Python 3 चाहिए और फ़ंक्शन लिखना आना चाहिए। अभी pytest की ज़रूरत नहीं है; इस अध्याय की हर चीज़ सादे पायथन से चलती है।
टेस्ट लिखने से पहले
इस कोर्स के हर अध्याय में इसी नाम का एक सेक्शन है, और हर एक अपने विषय के लिए इन्हीं छह सवालों का जवाब देता है। ये सवाल इसी अध्याय से आते हैं, इसलिए यहाँ ये एक जगह दिए गए हैं। इन्हें सँभालकर रखिए; बाकी कोर्स इन्हें चेकलिस्ट की तरह इस्तेमाल करता है।
- ठीक-ठीक वादा क्या है? Contract: इन इनपुट के लिए यह आउटपुट; इन इनपुट के लिए यह एरर; और ये side effects, या कोई नहीं।
- क्या टेस्ट इस कोड को बुला सकता है? क्या यह अपने इनपुट arguments के रूप में लेता है और नतीजा लौटाता है — या कीबोर्ड पढ़ता है, प्रिंट करता है, और घड़ी देखता है?
- कौन-से केस? हर तरह के इनपुट से एक, हर किनारे (edge) के दोनों तरफ़, अमान्य इनपुट, खाली और बहुत बड़े।
- मुझे सही जवाब कैसे पता है? हाथ से या specification से निकाला गया — टेस्ट हो रहे कोड को चलाकर कभी नहीं।
- क्या-क्या तैयार होना चाहिए? पायथन वर्ज़न, एक environment, टेस्ट फ़ाइलें कहाँ रहेंगी — और आगे के अध्यायों में, वे फ़ाइलें, सेटिंग्स और टूल जिनकी उस विषय को ज़रूरत है।
- मैं क्या टेस्ट नहीं करूँगा? ख़ुद पायथन, दूसरों की लाइब्रेरी, इतना सरल कोड कि ग़लत हो ही न सके, और वह व्यवहार जिसका किसी ने वादा नहीं किया।
यह रहा ऊपर वाले average पर इनका इस्तेमाल, जो इतना छोटा है कि पूरा किया जा सके।
Contract में एक कमी है, इसलिए पहला कदम उसे भरना है। मान लीजिए आप पूछते हैं, और जवाब मिलता है: "बिना अंकों का औसत बेमानी है; ValueError raise करो"। फ़ंक्शन पहले से टेस्ट करने लायक है — यह एक सूची लेता है और एक संख्या लौटाता है। इस अध्याय के लिए बस Python 3 तैयार होना चाहिए। और केस:
| केस | इनपुट | अपेक्षित | क्यों | | --- | --- | --- | --- | | सामान्य | [80, 90, 70] | 80.0 | आम इस्तेमाल: (80 + 90 + 70) / 3 | | सिर्फ़ एक अंक | [100] | 100.0 | सबसे छोटी सूची जिसका औसत होता है | | खाली | [] | ValueError | वही केस जो टूटा था; अब तय हो चुका |
सूची में क्या नहीं है: sum सही जोड़ता है या नहीं, या / भाग देता है या नहीं — यह पायथन का काम है, और इसे उससे कहीं ज़्यादा अच्छी तरह टेस्ट किया गया है जितना आप या मैं कभी करेंगे।
अध्याय का बाकी हिस्सा इन छह सवालों को एक-एक करके लेता है, और अंत में पूरी प्रक्रिया को एक असली जैसे फ़ंक्शन पर चलाता है।
टेस्ट असल में क्या है
टेस्ट व्यवहार के बारे में एक ऐसा दावा है जिसे चलाया जा सके: यह इनपुट दिया, तो यह नतीजा अपेक्षित है। "नतीजा" ठीक तीन तरह का होता है:
- एक return value —
average([80, 90, 70])देता है80.0 - एक एरर —
average([])raise करता हैValueError - एक side effect —
add_score(scores, 70)के बाद सूचीscoresके अंत में70है
दावा लिखने के लिए पायथन में पहले से एक स्टेटमेंट है: assert। शर्त सच हो तो यह कुछ नहीं करता, और झूठ हो तो AssertionError raise करता है। यह रहा हर तरह का एक दावा, जहाँ average अब अपने तय किए गए contract का पालन करता है:
def average(scores):
if not scores:
raise ValueError("average() of an empty list")
return sum(scores) / len(scores)
def add_score(scores, new_score):
scores.append(new_score)
# 1. A return value: given this input, expect this output.
assert average([80, 90, 70]) == 80.0
# 2. An error: given this input, expect this exception.
try:
average([])
except ValueError:
pass
else:
raise AssertionError("average([]) should raise ValueError")
# 3. A side effect: given this call, expect this change to the world.
scores = [80, 90]
add_score(scores, 70)
assert scores == [80, 90, 70]
print("3 claims checked")3 claims checkedध्यान दीजिए कि हर दावे को क्या चाहिए: एक इनपुट जो आप चुनते हैं, एक नतीजा जो आपको पहले से पता है, और कोड को बुलाकर यह देखने का तरीका कि उसने क्या किया। छह सवाल इसीलिए हैं कि शुरू करने से पहले ये तीनों आपके पास हों। यह भी देखिए कि एरर वाली जाँच कितनी भद्दी है — "यह raise होना चाहिए" कहने के लिए try/except/else की पाँच लाइनें। अध्याय चार में pytest इसे एक लाइन बना देता है।
1. Contract: धुँधली ज़रूरत से सटीक वादे तक
ज़रूरतें अक्सर एक वाक्य के रूप में आती हैं: "सदस्यों को बड़े ऑर्डर पर 10% छूट मिलती है।" सुनने में पूरा लगता है। इसे दो सावधान डेवलपरों को दीजिए और देखिए:
# "Members get 10% off big orders." Two honest readings of the same sentence.
def discount_by_asha(total, is_member):
if is_member and total > 1000:
return total * 0.10
return 0
def discount_by_ravi(total, is_member):
if is_member and total >= 1000:
return round(total * 0.10)
return 0
for total in [500, 1000, 1234.56]:
print(total, discount_by_asha(total, True), discount_by_ravi(total, True))500 0 0
1000 0 100
1234.56 123.456 123दोनों में से किसी ने ग़लती नहीं की। वाक्य ने यह नहीं बताया कि ठीक 1000 "बड़ा" गिना जाएगा या नहीं, या राउंड कैसे करना है। दोनों ने यह कमी अलग-अलग तरह से भरी, और 1000 के ऑर्डर को एक से 0 छूट मिलती है और दूसरे से 100। कोई टेस्ट नहीं बता सकता कि दोनों में कौन सही है, क्योंकि "सही" कभी परिभाषित ही नहीं किया गया।
Contract वह ज़रूरत है जिसे इतना सटीक बना दिया गया हो कि उसे टेस्ट किया जा सके। वाक्य से contract तक पहुँचने के लिए सवाल पूछिए — हर बार वही सवाल:
- इनपुट। कौन-से टाइप? कौन-सी रेंज? कोई सीमा है, और क्या सीमा ख़ुद शामिल है? शून्य, ऋणात्मक, खाली,
Noneका क्या? - आउटपुट। ठीक-ठीक क्या लौटता है — छूट, या नया कुल? कौन-सा टाइप? किस हद तक राउंड?
- एरर। कौन-से इनपुट अस्वीकार होते हैं, और कैसे — कौन-सा exception?
- Side effects। क्या यह कुछ बदलता है — पास की गई सूची, कोई फ़ाइल, डेटाबेस — या कुछ प्रिंट करता है? या कुछ भी नहीं?
छूट के बारे में पूछे जाने पर ये सवाल जवाब देते हैं, और जवाब वहीं जाते हैं जहाँ कोड है — docstring में:
def member_discount(total, is_member):
"""Return the discount on an order, in currency units.
- total: the order total before discount, a number >= 0.
A negative total raises ValueError.
- is_member: True or False.
- Members get 10% of the total when the total is 1000 or more
(1000 itself qualifies). Everyone else gets 0.
- The result is rounded to 2 decimal places.
- Returns the discount amount, not the new total.
- No side effects: reads nothing, prints nothing, changes nothing.
"""
if total < 0:
raise ValueError(f"total must not be negative, got {total}")
if is_member and total >= 1000:
return round(total * 0.10, 2)
return 0
print(member_discount(999.99, True))
print(member_discount(1000, True))
print(member_discount(1234.56, True))
print(member_discount(5000, False))
try:
member_discount(-5, True)
except ValueError as error:
print("ValueError:", error)0
100.0
123.46
0
ValueError: total must not be negative, got -5अब docstring की हर लाइन ऐसी चीज़ है जिसे टेस्ट जाँच सकता है, और हर जाँच का अपेक्षित मान साफ़ है। अच्छे contract की कसौटी यही है: क्या कोई अजनबी, कोड पढ़े बिना, किसी भी इनपुट का अपेक्षित मान लिख सकता है? अगर नहीं, तो अभी कोई सवाल पूछना बाकी है।
सवालों का जवाब कौन देता है? जिसकी वह ज़रूरत है — प्रोडक्ट ओनर, शिक्षक, क्लाइंट, या आप ख़ुद। जब पूछने के लिए कोई न हो, तो तय कीजिए, फ़ैसला contract में लिखिए, और आगे बढ़िए। लिखा हुआ फ़ैसला रिव्यू हो सकता है और बदला जा सकता है; चुपचाप लिया गया फ़ैसला बस कोड में पड़ा रहता है, जब तक किसी को चौंका न दे।
2. टेस्ट करने लायक कोड: pure core और पतला shell
टेस्ट किसी फ़ंक्शन को चुने हुए इनपुट से बुलाता है और जो वापस आता है उसकी तुलना करता है। कुछ फ़ंक्शन ऐसा होने ही नहीं देते। यह रहा एक, एक कैफ़े के happy hour के लिए — 17:00 से 19:00 के बीच ड्रिंक्स पर 20% छूट:
from datetime import datetime
def drink_price():
price = float(input("Price: "))
hour = datetime.now().hour
if 17 <= hour < 19:
price = price * 0.8
print(f"You pay {price:.2f}")चलाकर कीमत टाइप करें तो यह काम करता है। अब इसे दूसरी फ़ाइल, check_drinks.py, से जाँचने की कोशिश कीजिए:
from drinks import drink_price
result = drink_price()
print("returned:", result)ऑटोमेटेड जाँच तब चलती है जब कीबोर्ड पर कोई नहीं होता। < /dev/null प्रोग्राम को ठीक यही देता है — एक ऐसा इनपुट जिसमें कुछ नहीं है:
$ python check_drinks.py < /dev/null
Price: Traceback (most recent call last):
File "/home/you/cafe/check_drinks.py", line 3, in <module>
result = drink_price()
^^^^^^^^^^^^^
File "/home/you/cafe/drinks.py", line 5, in drink_price
price = float(input("Price: "))
^^^^^^^^^^^^^^^^
EOFError: EOF when reading a lineइसे कीमत दीजिए तो यह चल जाता है — लेकिन देखिए क्या वापस आता है:
$ echo 10 | python check_drinks.py
Price: You pay 10.00
returned: Noneतीन अलग समस्याएँ, और हर एक आम है:
- यह अपना इनपुट ख़ुद पढ़ता है
input()से, उसे argument के रूप में लेने के बजाय, इसलिए टेस्ट इनपुट चुन नहीं सकता। - यह अपना नतीजा प्रिंट करता है लौटाने के बजाय, इसलिए टेस्ट को
Noneमिलता है और तुलना के लिए कुछ नहीं होता। - यह घड़ी ख़ुद पढ़ता है। वह रन सुबह सात बजने से ठीक पहले हुआ था, इसलिए जवाब
10.00था; शाम साढ़े पाँच बजे वही कमांड8.00देती है। इस पर बनी जाँच इस पर निर्भर करती कि वह कब चली, और उसी हिसाब से पास या फ़ेल होती — एक flaky टेस्ट, जो न होने से भी बुरा है, क्योंकि लोग उसे अनदेखा करना सीख जाते हैं।
इलाज कोई टेस्टिंग की तरकीब नहीं है। यह आकार का बदलाव है: फ़ैसले को इनपुट और आउटपुट से अलग कीजिए। फ़ैसला एक pure फ़ंक्शन बन जाता है — उसे जो कुछ चाहिए वह arguments के रूप में आता है, जवाब return value के रूप में जाता है, और वह किसी और चीज़ को नहीं छूता। कीबोर्ड, स्क्रीन और घड़ी उसके चारों ओर एक पतले shell में चले जाते हैं:
from datetime import datetime
def drink_price(price, hour):
"""Return the price to pay for a drink.
20% off from 17:00 up to, but not including, 19:00.
"""
if 17 <= hour < 19:
return round(price * 0.8, 2)
return price
def main():
# The thin shell: all the input, output and clock reading lives here.
price = float(input("Price: "))
print(f"You pay {drink_price(price, datetime.now().hour):.2f}")
if __name__ == "__main__":
main()अब जाँचें दिन के किसी भी समय, जो घंटा चाहें चुन सकती हैं:
from drinks import drink_price
assert drink_price(10, 16) == 10
assert drink_price(10, 17) == 8.0
assert drink_price(10, 18) == 8.0
assert drink_price(10, 19) == 10
print("all checks passed")all checks passedकीबोर्ड पर बैठे इंसान के लिए प्रोग्राम ठीक पहले जैसा ही बर्ताव करता है। बदला यह है कि जो हिस्सा टेस्ट करने लायक है — नियम — उसे अब अलग से टेस्ट किया जा सकता है, और shell इतना पतला है कि उसमें ग़लती की गुंजाइश लगभग नहीं बचती।
टेस्ट करने में मुश्किल कोड के चेतावनी संकेत, ताकि आप टेस्ट लिखने से पहले उन्हें पहचान सकें:
| संकेत | यह टेस्ट को क्यों चोट पहुँचाता है | आम इलाज | | --- | --- | --- | | लॉजिक के अंदर input() | टेस्ट इनपुट नहीं चुन सकता | इसे argument के रूप में लें | | इकलौता नतीजा print() | तुलना के लिए कुछ वापस नहीं आता | मान लौटाएँ; प्रिंट shell में करें | | अंदर datetime.now(), random | जवाब हर रन में बदलता है | समय या random मान बाहर से दें | | global variable पढ़ता या बदलता है | टेस्ट एक-दूसरे पर असर डालते हैं | उसे पास करें, नया मान लौटाएँ | | अंदर कोई तय फ़ाइल या URL खोलता है | टेस्ट को वह फ़ाइल या नेटवर्क चाहिए | डेटा, या path, बाहर से दें |
कोड को हमेशा नए आकार में नहीं ढाला जा सकता — कभी वह किसी और का होता है, या I/O ही उसका व्यवहार होता है। ऐसे मामलों के लिए pytest के पास टूल हैं: capsys प्रिंट हुआ आउटपुट पकड़ता है (अध्याय नौ), tmp_path एक अस्थायी फ़ोल्डर देता है (अध्याय नौ), और monkeypatch व mocks घड़ी, नेटवर्क और environment को बदल देते हैं (अध्याय दस और ग्यारह)। लेकिन पहले आकार बदलना आता है। जिस कोड को टेस्ट करना आसान है, उसे समझना भी आमतौर पर आसान होता है।
3. केस चुनना
आप हर इनपुट टेस्ट नहीं कर सकते; अकेला drink_price हर घंटे पर हर कीमत लेता है। काम है एक छोटा सेट चुनना, जिसमें हर केस कोई ऐसी ग़लती पकड़ सके जो बाकी चूक जाते। ज़्यादातर काम दो विचार करते हैं।
Equivalence classes। उन इनपुट को एक समूह में रखिए जिनके साथ contract एक जैसा बर्ताव करता है। समूह का कोई भी एक सदस्य बाकियों का प्रतिनिधि है: अगर drink_price(10, 12) सही है, तो drink_price(10, 11) भी लगभग पक्का सही है, क्योंकि दोनों को कोड की एक ही लाइन संभालती है। मान लीजिए contract को कसकर यह कहा गया है कि 0–23 के बाहर का घंटा, या ऋणात्मक कीमत, ValueError raise करती है। तब घंटे पाँच classes में बँटते हैं:
hour: ... -2 -1 | 0 1 ... 15 16 | 17 18 | 19 20 ... 23 | 24 25 ...
invalid | full price | 20% off | full price | invalidBoundary values। ग़लतियाँ किसी class में बराबर नहीं फैलतीं। वे उसके किनारों पर जमा होती हैं, क्योंकि वहीं < और <= चुने जाते हैं, और वहीं "19:00 तक" को कोड में बदला जाता है। इसलिए हर किनारे के लिए उसके दोनों तरफ़ का मान टेस्ट कीजिए: एक class का आख़िरी और अगली का पहला। किसी सीमा L के लिए इसका मतलब है L - 1, L और L + 1 को देखना; गिनती और लंबाई के लिए 0 और 1 भी।
यह रहा कारण कि "हर class से एक सामान्य मान" काफ़ी क्यों नहीं है। नीचे happy-hour की शर्त एक आसान चूक के साथ लिखी गई है, 17 <= hour के बजाय 17 < hour:
def drink_price(price, hour):
# A slip: 17 < hour instead of 17 <= hour.
if 17 < hour < 19:
return round(price * 0.8, 2)
return price
# Three "typical" cases, one from each valid class:
assert drink_price(10, 12) == 10
assert drink_price(10, 18) == 8.0
assert drink_price(10, 21) == 10
print("typical cases passed")
# The boundary:
assert drink_price(10, 17) == 8.0
print("boundary passed")typical cases passed
Traceback (most recent call last):
File "/home/you/cafe/boundary.py", line 15, in <module>
assert drink_price(10, 17) == 8.0
^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionErrorहर class से एक केस: सब पास, और जो ग्राहक ठीक पाँच बजे आते हैं वे पूरी कीमत चुकाते हैं। boundary वाला केस इसे तुरंत पकड़ लेता है।
Classes और boundaries इनपुट का आकार कवर करते हैं। उसके बाद इन सवालों से गुज़रिए, जिनमें से हर एक ने असली बग पकड़े हैं:
- अमान्य इनपुट — जिसे contract अस्वीकार करने को कहता है: ऋणात्मक कीमत, घंटा
24। - खाली और शून्य —
[],"",0,0की कीमत। क्या "कुछ नहीं" ठीक बर्ताव करता है? None— सिर्फ़ तब, जब contract उसका ज़िक्र करे। अगर नहीं करता, तो वह केस नहीं है (सवाल छह देखिए)।- बड़े मान — बहुत लंबी सूची, बहुत बड़ी संख्या। जिस चीज़ की भी सीमा हो, उसे सीमा पर टेस्ट कीजिए।
- राउंडिंग — जहाँ हिसाब नतीजे से ज़्यादा दशमलव अंक पैदा करता है।
यह सब मिलाकर एक टेस्ट प्लान बनता है: हर केस के लिए एक पंक्ति, कोई भी टेस्ट कोड लिखने से पहले। आख़िरी कॉलम सबसे अहम है — जिस पंक्ति को आप सही ठहरा नहीं सकते, उसकी आपको ज़रूरत नहीं है।
| केस | इनपुट (price, hour) | अपेक्षित | क्यों | | --- | --- | --- | --- | | सबसे छोटा मान्य घंटा | (10, 0) | 10 | मान्य रेंज का किनारा | | पूरी कीमत का आख़िरी घंटा | (10, 16) | 10 | 17:00 वाले किनारे से नीचे | | छूट का पहला घंटा | (10, 17) | 8.0 | 17:00 वाले किनारे पर | | छूट का आख़िरी घंटा | (10, 18) | 8.0 | 19:00 वाले किनारे से नीचे | | फिर से पूरी कीमत का पहला घंटा | (10, 19) | 10 | 19:00 वाले किनारे पर — "शामिल नहीं" | | सबसे बड़ा मान्य घंटा | (10, 23) | 10 | मान्य रेंज का किनारा | | मुफ़्त ड्रिंक | (0, 17) | 0 | शून्य कीमत | | राउंडिंग | (2.49, 17) | 1.99 | 2.49 × 0.8 = 1.992, 2 अंकों तक राउंड | | घंटा बहुत कम | (10, -1) | ValueError | रेंज के ठीक बाहर | | घंटा बहुत ज़्यादा | (10, 24) | ValueError | रेंज के ठीक बाहर | | ऋणात्मक कीमत | (-1, 12) | ValueError | अमान्य इनपुट |
और यह योजना, कसे हुए फ़ंक्शन के ख़िलाफ़ जाँचों में बदली हुई:
def drink_price(price, hour):
"""Return the price to pay for a drink.
- price: a number >= 0. hour: a whole number 0-23.
- 20% off from 17:00 up to, but not including, 19:00.
- The result is rounded to 2 decimal places.
- An hour outside 0-23 or a negative price raises ValueError.
"""
if not 0 <= hour <= 23:
raise ValueError(f"hour must be 0-23, got {hour}")
if price < 0:
raise ValueError(f"price must not be negative, got {price}")
if 17 <= hour < 19:
return round(price * 0.8, 2)
return price
def raises_value_error(price, hour):
try:
drink_price(price, hour)
except ValueError:
return True
return False
assert drink_price(10, 0) == 10 # lowest valid hour
assert drink_price(10, 16) == 10 # last full-price hour before
assert drink_price(10, 17) == 8.0 # first discounted hour
assert drink_price(10, 18) == 8.0 # last discounted hour
assert drink_price(10, 19) == 10 # first full-price hour after
assert drink_price(10, 23) == 10 # highest valid hour
assert drink_price(0, 17) == 0 # free drink stays free
assert drink_price(2.49, 17) == 1.99 # rounding: 1.992 -> 1.99
assert raises_value_error(10, -1) # just below the valid range
assert raises_value_error(10, 24) # just above the valid range
assert raises_value_error(-1, 12) # negative price
print("11 checks passed")11 checks passedग्यारह पंक्तियाँ, और हर एक किसी ऐसे कारण से है जिसे आप ज़ोर से बोलकर बता सकते हैं।
4. Oracle: कोड के बिना जवाब जानना
हर जाँच कोड के जवाब की तुलना एक अपेक्षित जवाब से करती है। अपेक्षित जवाब का स्रोत oracle कहलाता है, और उसका एक ही नियम है: वह टेस्ट हो रहा कोड नहीं होना चाहिए।
यह बात कहने लायक भी नहीं लगती, जब तक आप यह न देख लें कि इसे तोड़ना कितना आसान है। यहाँ member_discount में एक बग है — 10% के बजाय 1% — और दो जाँचें हैं:
def member_discount(total, is_member):
if total < 0:
raise ValueError(f"total must not be negative, got {total}")
if is_member and total >= 1000:
return round(total * 0.01, 2) # bug: 1%, not 10%
return 0
# Wrong: the expected value comes from the code under test.
expected = member_discount(2000, True)
assert member_discount(2000, True) == expected
print("self-check passed, expected was", expected)
# Right: the expected value was worked out by hand. 10% of 2000 is 200.
assert member_discount(2000, True) == 200
print("hand-check passed")self-check passed, expected was 20.0
Traceback (most recent call last):
File "/home/you/shop/oracle.py", line 15, in <module>
assert member_discount(2000, True) == 200
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionErrorपहली जाँच फ़ंक्शन की तुलना ख़ुद उसी से करती है। यह सही फ़ंक्शन के लिए पास होती है, इस टूटे हुए के लिए भी, और किसी के भी लिखे किसी भी दूसरे वर्ज़न के लिए भी — यह फ़ेल हो ही नहीं सकती, इसलिए यह आपको कुछ नहीं बताती।
इतने खुले रूप में इसे कोई नहीं लिखता। यही ग़लती आमतौर पर दो भेसों में से किसी एक में आती है:
- आउटपुट कॉपी करना। आप फ़ंक्शन चलाते हैं, वह
20.0प्रिंट करता है, और आप20.0जाँच में चिपका देते हैं। अगर कोड पहले से ग़लत था, तो अब जाँच बग की रखवाली कर रही है। - फ़ॉर्मूला दोहराना। आप अपेक्षित मान
2000 * 0.01लिखते हैं — वही हिसाब जो कोड करता है। कोड के लॉजिक की नकल कोड की ग़लतियों में भी हिस्सेदार होती है। (नीचे "जब यह काम न करे" में इसे चलता हुआ दिखाया गया है।)
अच्छे oracle, मोटे तौर पर सबसे आम से सबसे कम आम तक:
- Contract से हाथ से निकालना। ऐसे इनपुट चुनिए जो इसे आसान बनाएँ: 1873.41 के बजाय 2000, ताकि "इसका 10%" आप मन में निकाल सकें।
- Specification के उदाहरण। अगर ज़रूरत कहती है "1500 के ऑर्डर पर 150 छूट", तो वह पंक्ति एक टेस्ट है।
- जाने-माने तथ्य। पानी 100 °C और 212 °F पर उबलता है; खाली सूची की लंबाई 0 है।
- कोई दूसरा, भरोसेमंद तरीका। एक सरल, धीमा हिसाब, या standard library का कोई फ़ंक्शन जो वही काम दूसरे तरीके से करता है:
import statistics
def average(scores):
if not scores:
raise ValueError("average() of an empty list")
return sum(scores) / len(scores)
samples = [[80, 90, 70], [1, 2], [5], [10, 0, 0, 0]]
for scores in samples:
assert average(scores) == statistics.mean(scores), scores
print("agrees with statistics.mean on", len(samples), "samples")agrees with statistics.mean on 4 samplesstatistics.mean दूसरे लोगों ने, दूसरे तरीके से लिखा है, इसलिए उससे सहमत होने का कुछ मतलब है। (दशमलव नतीजों की == से तुलना धोखा दे सकती है — पायथन में 0.1 + 0.2 == 0.3 का नतीजा False है। यहाँ के मान सुरक्षित हैं; floats की ठीक से तुलना करना अध्याय पाँच में है।)
अगर इनमें से किसी भी तरीके से आप अपेक्षित जवाब नहीं निकाल पाते, तो यह टेस्टिंग की समस्या नहीं है। इसका मतलब है कि आपको अभी पता ही नहीं कि कोड को क्या करना चाहिए — contract पर वापस जाइए।
5. क्या-क्या तैयार होना चाहिए
इस अध्याय के लिए सिर्फ़ Python 3। देखिए आपके पास कौन-सा है:
$ python3 --version
Python 3.12.3यह कोर्स Python 3.12 इस्तेमाल करता है, और 3.10 या उससे ऊपर का कोई भी वर्ज़न इसका लगभग सब कुछ चला देगा। Windows पर कमांड आमतौर पर py --version होती है।
अगले अध्याय से, टेस्ट चलने से पहले तीन और चीज़ें तैयार होनी चाहिए, और अध्याय एक इनमें से हर एक को सेट करता है:
- प्रोजेक्ट के लिए एक virtual environment, ताकि टेस्टिंग टूल उसी पायथन में इंस्टॉल हो जो आपका कोड चलाता है।
- उसमें pytest इंस्टॉल हो।
- टेस्ट के लिए एक जगह। टेस्ट फ़ाइलें कोड के बगल में, या
tests/फ़ोल्डर में रहती हैं, और उनके नामtest_से शुरू होते हैं। अध्याय एक पहले तरीके से शुरू करता है; अध्याय तीन दूसरे पर जाता है और बताता है क्यों।
आगे के अध्याय अपनी ज़रूरतें जोड़ते हैं — एक कॉन्फ़िगरेशन फ़ाइल, एक plugin, डिस्क पर सैंपल डेटा — और हर एक उन्हें अपने "टेस्ट लिखने से पहले" में बताता है।
6. क्या टेस्ट न करें, और कब रुकें
जो टेस्ट आपके कोड की कोई ग़लती पकड़ ही नहीं सकता, वह लिखने में समय लेता है, चलने में समय लेता है, सँभालने में समय लेता है, और बदले में कुछ नहीं देता। इन्हें छोड़ दीजिए:
- ख़ुद पायथन। यह नहीं कि
sortedसॉर्ट करता है याroundराउंड करता है। यह टेस्ट कीजिए कि आपका फ़ंक्शन सही चीज़ को सही क्रम में सॉर्ट करता है। - दूसरों की लाइब्रेरी। यह नहीं कि
requestsरिक्वेस्ट भेजता है याpandasCSV पढ़ता है। यह टेस्ट कीजिए कि आपका कोड नतीजे के साथ क्या करता है — और अगर किसी लाइब्रेरी के व्यवहार के बारे में पक्का होना हो, तो आपकी धारणा को दर्ज करने वाली एक छोटी जाँच काफ़ी है। - इतना सरल कोड कि ग़लत हो ही न सके। एक फ़ंक्शन जो कोई constant लौटाता है, या एक class जो सिर्फ़ अपने arguments सहेजती है। अगर आप ग़लती की कल्पना ही नहीं कर सकते, तो टेस्ट उसे पकड़ नहीं सकता।
- वह व्यवहार जिसका किसी ने वादा नहीं किया। अगर contract
drink_priceको string पास करने के बारे में कुछ नहीं कहता, तो टेस्ट बस उसी को जमा देगा जो वह आज संयोग से करता है। या तो इसे contract में जोड़िए — फिर टेस्ट कीजिए — या छोड़ दीजिए। - "क्या" के बजाय "कैसे"। यह नहीं कि फ़ंक्शन ने अंदर कौन-सा helper बुलाया, या किस क्रम में अपने कदम उठाए। यह टेस्ट कीजिए कि क्या अंदर जाता है और क्या बाहर आता है; तब अंदर का हिस्सा बेझिझक दोबारा लिखा जा सकता है, और टेस्ट साबित करते हैं कि वह अब भी काम करता है।
और कितने काफ़ी हैं? कोई तय संख्या नहीं है, लेकिन एक नियम है। आपके पास काफ़ी टेस्ट तब हैं जब:
- हर equivalence class का कम से कम एक केस है,
- हर boundary के दोनों तरफ़ एक-एक केस है,
- contract जिस हर एरर का वादा करता है, उसका एक केस है,
- और आपने जो भी बग कभी ठीक किया है, उसका अपना एक केस है, ताकि वह चुपचाप लौट न आए।
फिर रुक जाइए। हर नए केस के लिए, जिसे जोड़ने का मन हो, पूछिए: यह कौन-सी ऐसी ग़लती पकड़ेगा जो बाकी नहीं पकड़ते? अगर आप कोई नाम नहीं बता सकते, तो यह दोहराव है। drink_price(10, 12) के बगल में drink_price(10, 11) कुछ नया नहीं पकड़ता।
पूर्ण उदाहरण
अब पूरी प्रक्रिया, शुरू से अंत तक, एक असली जैसे फ़ंक्शन पर।
ज़रूरत, जैसी आई थी: "लोकल पार्सल की शिपिंग 60 है और नेशनल की 120। भारी पार्सल ज़्यादा महँगे हैं — लोकल में हर अतिरिक्त किलो के 20, नेशनल में 30। 30 kg से ऊपर का कुछ भी हम नहीं लेते।"
सवाल, और उन्हें मिले जवाब:
| सवाल | जवाब | | --- | --- | | बेस कीमत किसे कवर करती है? | पहले किलोग्राम को — ठीक 1 kg अब भी बेस कीमत है। | | 1.2 kg का चार्ज 1 kg जितना है या 2 जितना? | हर शुरू हुआ किलोग्राम गिना जाता है: 1.2 kg का चार्ज 2 kg जितना है। | | क्या ठीक 30 kg स्वीकार है? | हाँ। 30 की अनुमति है; उससे ऊपर कुछ भी अस्वीकार। | | 0 kg, या ऋणात्मक वज़न का क्या? | अस्वीकार — यह डेटा एंट्री की ग़लती है। | | ज़ोन कैसे लिखे जाते हैं? | ठीक "local" या "national"। बाकी सब अस्वीकार, "Local" भी। | | क्या वापस आता है? | लागत, एक पूर्ण संख्या के रूप में। कुछ प्रिंट या सेव नहीं होता। |
Contract और टेस्ट करने लायक फ़ंक्शन, shipping.py। यह सब कुछ arguments के रूप में लेता है और एक संख्या लौटाता है; अलग करने के लिए कोई shell नहीं है, क्योंकि इसमें कोई इनपुट या आउटपुट है ही नहीं:
import math
BASE = {"local": 60, "national": 120} # covers the first kilogram
PER_EXTRA_KG = {"local": 20, "national": 30}
MAX_WEIGHT_KG = 30
def shipping_cost(weight_kg, zone):
"""Return the shipping cost of one parcel, as a whole number.
- weight_kg: a number. At or below 0, or above 30: ValueError.
- zone: "local" or "national", exactly. Anything else: ValueError.
- The base price covers the first kilogram (1 kg included).
- Every started kilogram after that costs extra: 1.2 kg is charged as 2 kg.
- No side effects: reads nothing, prints nothing, changes nothing.
"""
if zone not in BASE:
raise ValueError(f"unknown zone: {zone!r}")
if not 0 < weight_kg <= MAX_WEIGHT_KG:
raise ValueError(f"weight must be more than 0 and at most 30 kg, got {weight_kg}")
extra_kg = math.ceil(weight_kg) - 1
return BASE[zone] + extra_kg * PER_EXTRA_KG[zone]math.ceil अगली पूर्ण संख्या तक ऊपर राउंड करता है — math.ceil(1.2) है 2 — जो ठीक-ठीक "हर शुरू हुआ किलोग्राम गिना जाता है" है।
केस टेबल। वज़न की classes हैं: अमान्य (0 और नीचे), बेस किलोग्राम (0 से ऊपर, 1 तक), अतिरिक्त किलोग्राम (1 से ऊपर, 30 तक), अमान्य (30 से ऊपर)। ज़ोन हैं "local", "national", और बाकी सब कुछ। हर अपेक्षित मान contract से हाथ से निकाला गया है, कोड से नहीं:
| केस | इनपुट | अपेक्षित | क्यों | | --- | --- | --- | --- | | हल्का पार्सल | (0.5, "local") | 60 | बेस किलोग्राम के अंदर | | ठीक 1 kg | (1, "local") | 60 | बेस का ऊपरी किनारा: 1 kg शामिल है | | 1 kg से थोड़ा ऊपर | (1.2, "local") | 80 | शुरू हुआ किलोग्राम गिना जाता है: 60 + 1 × 20 | | पूरे किलो, लोकल | (3, "local") | 100 | 60 + 2 × 20 | | पूरे किलो, नेशनल | (3, "national") | 180 | 120 + 2 × 30 — दूसरा ज़ोन | | सीमा पर | (30, "national") | 990 | 120 + 29 × 30; 30 स्वीकार है | | शून्य | (0, "local") | ValueError | निचला किनारा: 0 अस्वीकार है | | ऋणात्मक | (-2, "local") | ValueError | अमान्य इनपुट | | सीमा से ऊपर | (30.5, "local") | ValueError | 30 से ठीक ऊपर | | ग़लत ज़ोन | (2, "Local") | ValueError | ठीक "local" नहीं |
टेबल में जान-बूझकर क्या नहीं है: None या "2" का वज़न। Contract कहता है कि वज़न एक संख्या है, इसलिए ये उसके बाहर हैं — 0 से तुलना होने पर पायथन अपनी तरफ़ से TypeError raise करता है, और इससे आगे हम कोई वादा नहीं करते। math.ceil भी नहीं: वह पायथन का है, हमारा नहीं। और (3, "local") के बगल में (2, "local") भी नहीं: वही class, कोड की वही लाइन, पकड़ने को कुछ नया नहीं।
जाँचें, check_shipping.py — टेबल, पंक्ति-दर-पंक्ति:
from shipping import shipping_cost
def raises_value_error(weight_kg, zone):
"""True if shipping_cost raises ValueError for these arguments."""
try:
shipping_cost(weight_kg, zone)
except ValueError:
return True
return False
# Valid weights: one case per class, and both sides of every edge.
assert shipping_cost(0.5, "local") == 60
assert shipping_cost(1, "local") == 60
assert shipping_cost(1.2, "local") == 80
assert shipping_cost(3, "local") == 100
assert shipping_cost(3, "national") == 180
assert shipping_cost(30, "national") == 990
# Invalid input: the contract promises a ValueError for each.
assert raises_value_error(0, "local")
assert raises_value_error(-2, "local")
assert raises_value_error(30.5, "local")
assert raises_value_error(2, "Local")
print("all 10 checks passed")$ python check_shipping.py
all 10 checks passedअब इसे तोड़िए। कुछ महीने बाद कोई shipping.py को साफ़-सुथरा करता है, import math हटा देता है और math.ceil(weight_kg) की जगह int(weight_kg) लिख देता है। पढ़ने में ठीक लगता है, और बिना एरर के चलता है। जाँचें फिर से चलाइए:
$ python check_shipping.py
Traceback (most recent call last):
File "/home/you/shop/check_shipping.py", line 14, in <module>
assert shipping_cost(0.5, "local") == 60
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionErrorयोजना ने अपना काम किया: जाँचें चलते ही बदलाव पकड़ा गया। int ऊपर राउंड करने के बजाय दशमलव काट देता है, इसलिए 0.5 kg बन गया int(0.5) - 1, यानी -1 अतिरिक्त किलोग्राम।
लेकिन रिपोर्ट को ऐसे पढ़िए जैसे आपको यह पहले से पता न हो, और देखिए कि यह कितना कम बताती है:
- असली मान क्या था? रिपोर्ट लाइन दिखाती है, संख्या नहीं। यह जानने के लिए कि वह
40था, आपकोshipping_cost(0.5, "local")ख़ुद चलाना पड़ेगा। - और क्या टूटा है? स्क्रिप्ट पहली विफलता पर रुक गई। 1.2 kg वाला केस भी ग़लत है — अब वह
80के बजाय60देता है — लेकिन वह जाँच कभी चली ही नहीं। - कौन-से वादे निभे? कुछ नहीं बताता कि एरर वाले केस अब भी पास होते हैं।
- और एरर वाली जाँचों को सिर्फ़ "यह raise होना चाहिए" कहने के लिए
try/exceptवाला helper फ़ंक्शन चाहिए था।
सोचना ही कठिन हिस्सा था, और वह हो चुका है: contract, टेबल, oracle। कमी है एक बेहतर runner की — जो जाँचें ख़ुद ढूँढे, विफलता के बाद भी हर एक चलाए, असली मान अपेक्षित मान के बगल में दिखाए, और एरर एक ही लाइन में जाँचे। वह pytest है, और उसे इंस्टॉल करना अगला अध्याय है।
जब यह काम न करे
ये पायथन की एरर के बजाय योजना की ग़लतियाँ हैं, इसलिए इनके लक्षण शांत होते हैं: आमतौर पर एक जाँच जो तब पास होती है जब उसे नहीं होना चाहिए।
अपेक्षित मान कोड का ही फ़ॉर्मूला दोहराता है जाँच वही हिसाब करके लिखी गई है जो फ़ंक्शन करता है। यहाँ फ़ंक्शन में ऊपर वाला int बग है, और जाँच में भी:
def shipping_cost(weight_kg, zone):
# Local zone only, to keep the example short. Bug: int() instead of math.ceil().
return 60 + (int(weight_kg) - 1) * 20
# The expected value repeats the code's own sum, so it repeats the code's own bug.
assert shipping_cost(1.2, "local") == 60 + (int(1.2) - 1) * 20
print("passed")
# Worked out by hand from the contract: 1.2 kg is charged as 2 kg, so 60 + 20.
assert shipping_cost(1.2, "local") == 80passed
Traceback (most recent call last):
File "/home/you/shop/rederive.py", line 11, in <module>
assert shipping_cost(1.2, "local") == 80
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError"implementation को टेस्ट करना" भी ऐसा ही दिखता है: जाँच यह बताने के बजाय कि कोड क्या वादा करता है, यह दोहराती है कि वह कैसे काम करता है। अपेक्षित मान contract से निकली एक सादी संख्या के रूप में लिखिए।
अपेक्षित मान आउटपुट से कॉपी किया गया आपने फ़ंक्शन चलाया, 20.0 देखा, और उसे जाँच में चिपका दिया। जाँच पास होती है — और अगर 20.0 ग़लत था, तो अब वह बग की रक्षा करती है। कोड चलाने से पहले हर अपेक्षित मान ख़ुद निकालिए; अगर आपकी संख्या और कोड की संख्या मेल न खाएँ, तो कुछ भी लिखने से पहले पता कीजिए कि कौन-सी ग़लत है।
योजना में कोई boundary केस नहीं है हर जाँच पास होती है, और बग किनारे पर बैठा है: पाँच बजे, ठीक 1 kg, उम्र 12। आमतौर पर इसका कारण ऐसी टेबल होती है जिसमें सिर्फ़ "सामान्य" पंक्तियाँ हों। Contract में हर <, <=, "तक", "से" और "कम से कम" के लिए, दोनों तरफ़ एक-एक केस होना चाहिए।
फ़ंक्शन को कोई जाँच बुला नहीं सकती EOFError: EOF when reading a line का मतलब है कि फ़ंक्शन कीबोर्ड का इंतज़ार करता है; returned: None का मतलब है कि उसने अपना जवाब लौटाने के बजाय प्रिंट कर दिया; सुबह पास और शाम को फ़ेल होने वाली जाँच का मतलब है कि वह घड़ी पढ़ता है। तीनों डिज़ाइन की समस्याएँ हैं, और सेक्शन दो वाले pure-core-और-पतले-shell के बदलाव से ठीक होती हैं।
SyntaxWarning: assertion is always true, perhaps remove parentheses? assert और उसके मैसेज के चारों ओर कोष्ठक लगाने से दोनों मिलकर एक tuple बन जाते हैं, और ख़ाली न हो ऐसा tuple हमेशा सच होता है:
def drink_price(price, hour):
if 17 <= hour < 19:
return round(price * 0.8, 2)
return price
assert (drink_price(10, 17) == 999, "happy hour price")
print("passed?!")/home/you/cafe/tuple.py:7: SyntaxWarning: assertion is always true, perhaps remove parentheses?
assert (drink_price(10, 17) == 999, "happy hour price")
passed?!999 साफ़ तौर पर ग़लत है, और जाँच पास हो गई। assert drink_price(10, 17) == 999, "happy hour price" लिखिए — बिना कोष्ठक के — और यह वैसे ही फ़ेल होगी जैसे इसे होना चाहिए। पायथन यहाँ आपको चेतावनी देता है; उसे स्क्रॉल करके आगे बढ़ने के बजाय पढ़िए।
दशमलव नतीजों की == से तुलना करने वाली जाँच सही जवाब पर भी फ़ेल होती है पायथन में 0.1 + 0.2 == 0.3 का नतीजा False है, क्योंकि 0.1 + 0.2 असल में 0.30000000000000004 है। योजना का सबक यह है कि contract में बताइए कि नतीजे कैसे राउंड होते हैं, जैसा member_discount और drink_price करते हैं। floats की तुलना के लिए pytest का टूल अध्याय पाँच में है।
चरण 4 / 6 — अनुमान
अपनी समझ की जाँच करें
Requirement: "500 या उससे ज़्यादा के ऑर्डर पर डिलीवरी फ़्री।" यह स्क्रिप्ट चलाने पर क्या होगा?
def free_shipping(total):
return total > 500
assert free_shipping(800) is True
assert free_shipping(100) is False
print("checks passed")
assert free_shipping(500) is True
print("all done")- A`checks passed` और फिर `all done` प्रिंट होते हैं
- B`checks passed` प्रिंट होता है, फिर आख़िरी assert पर `AssertionError`
- Cपहले assert पर ही `AssertionError`, कुछ प्रिंट नहीं होता
- Dकुछ नहीं होता — स्क्रिप्ट में assert अनदेखे किए जाते हैं
कौन-सा फ़ंक्शन जैसा है वैसा ही, एक लाइन के assert से भरोसेमंद ढंग से टेस्ट हो सकता है?
- Adef greet(): print('Hi', input())
- Bdef is_weekend(): return datetime.now().weekday() >= 5
- Cdef is_weekend(weekday): return weekday >= 5
- Ddef log_visit(): VISITS.append(1)
इस चेक का काम "1.2 kg को 2 kg गिना जाएगा" नियम की रखवाली करना है। इसमें गड़बड़ क्या है?
from shipping import shipping_cost
expected = shipping_cost(1.2, "local")
assert shipping_cost(1.2, "local") == expected- Aअपेक्षित मान उसी कोड से आया है जिसे टेस्ट किया जा रहा है, इसलिए चेक कभी फ़ेल नहीं हो सकता
- B`1.2` कोई boundary value नहीं है, इसलिए इसे टेस्ट करने का मतलब नहीं
- C`==` की जगह `is` लिखना चाहिए
- Dफ़ंक्शन को `try`/`except` के अंदर बुलाना चाहिए
उत्तर देने के लिए अकाउंट आवश्यक है
अपने उत्तर जाँचने के लिए साइन इन करें
प्रश्न ऊपर दिए गए हैं, और मन में उत्तर सोचना ही मुख्य कार्य है। सही उत्तर, व्याख्या और तीन-स्तरीय संकेत देखने के लिए साइन इन करें।
आपकी बारी
एक सिनेमा आपको यह ज़रूरत भेजता है:
"बच्चे कम देते हैं, पेंशनभोगियों को छूट मिलती है, और शिशु मुफ़्त जाते हैं। वयस्क पूरी कीमत देते हैं, 300।"
एक भी जाँच लिखने से पहले इस अध्याय की हर चीज़ क्रम से कीजिए:
- सवाल पूछिए। कम से कम पाँच बातें लिखिए जिन्हें वाक्य खुला छोड़ देता है — "बच्चे" का मतलब क्या है, "पेंशनभोगी" कहाँ से शुरू होते हैं, किनारे शामिल हैं या नहीं, कीमतें क्या हैं, असंभव उम्र पर क्या होता है। फिर जवाब तय कीजिए, जैसे सिनेमा करता: 3 से कम मुफ़्त; 3 से 12 वाले 150 देते हैं; 13 से 64 वाले 300; 65 और ऊपर वाले 200; उम्र 0 से 120 तक होती है, और उसके बाहर कुछ भी
ValueErrorraise करता है। - Contract लिखिए,
cinema.pyमें फ़ंक्शनticket_price(age)के docstring के रूप में। - इसे टेस्ट करने लायक बनाइए: arguments अंदर, return value बाहर, न
input()नprint()। - केस टेबल लिखिए: केस, इनपुट, अपेक्षित, क्यों। पहले equivalence classes ढूँढिए, फिर हर किनारे के दोनों तरफ़ एक केस रखिए। हर अपेक्षित मान contract से निकालिए।
- तय कीजिए कि आप क्या टेस्ट नहीं करेंगे, और क्यों।
check_cinema.pyलिखिए, हर पंक्ति के लिए एक सादाassert, और उसे चलाइए।
फिर फ़ंक्शन को जान-बूझकर तोड़िए: बच्चों वाला नियम "12 और उससे कम" से बदलकर "12 से कम" कर दीजिए — <= को <। चलाने से पहले अनुमान लगाइए कि कौन-सी जाँच फ़ेल होगी। चलाइए और मिलाइए।
समाधान
1. सवाल, और तय किए गए जवाब:
| सवाल | जवाब | | --- | --- | | शिशु किस उम्र तक मुफ़्त है? | 0, 1 और 2 साल। 3 से टिकट के पैसे लगते हैं। | | "बच्चा" कौन है? | 3 से 12 साल, दोनों शामिल। कीमत 150। | | क्या 13 साल का बच्चा है? | नहीं — 13 वयस्क कीमत देता है, 300। | | पेंशनभोगी कहाँ से शुरू होते हैं? क्या ठीक 65 शामिल है? | 65 और ऊपर। कीमत 200। | | असंभव उम्र क्या है? | 0 से कम या 120 से ज़्यादा: ValueError। | | क्या वापस आता है? | कीमत, एक पूर्ण संख्या के रूप में; कुछ प्रिंट नहीं होता। |
2 और 3. Contract और फ़ंक्शन, cinema.py। किनारे नाम वाले constants हैं, इसलिए कोड टेबल की तरह पढ़ा जाता है:
FREE_UNDER = 3 # ages 0-2 go free
CHILD_UP_TO = 12 # ages 3-12 pay the child price
SENIOR_FROM = 65 # ages 65 and over pay the senior price
MAX_AGE = 120
def ticket_price(age):
"""Return the price of one cinema ticket, as a whole number.
- age: a whole number of years, 0 to 120. Outside that range: ValueError.
- 0-2: 0 (free), 3-12: 150, 13-64: 300, 65 and over: 200.
- No side effects: reads nothing, prints nothing, changes nothing.
"""
if not 0 <= age <= MAX_AGE:
raise ValueError(f"age must be 0-{MAX_AGE}, got {age}")
if age < FREE_UNDER:
return 0
if age <= CHILD_UP_TO:
return 150
if age < SENIOR_FROM:
return 300
return 2004. केस टेबल। Classes हैं: अमान्य (0 से कम), मुफ़्त (0–2), बच्चा (3–12), वयस्क (13–64), वरिष्ठ (65–120), अमान्य (120 से ऊपर)। इससे चार सामान्य केस, आठ boundary केस और दो अमान्य केस बनते हैं:
| केस | इनपुट | अपेक्षित | क्यों | | --- | --- | --- | --- | | सामान्य शिशु | 1 | 0 | मुफ़्त class | | सामान्य बच्चा | 8 | 150 | बच्चा class | | सामान्य वयस्क | 30 | 300 | वयस्क class | | सामान्य वरिष्ठ | 80 | 200 | वरिष्ठ class | | नवजात | 0 | 0 | सबसे छोटी मान्य उम्र | | आख़िरी मुफ़्त उम्र | 2 | 0 | मुफ़्त/बच्चा किनारे से नीचे | | पहली बच्चा उम्र | 3 | 150 | मुफ़्त/बच्चा किनारे पर | | आख़िरी बच्चा उम्र | 12 | 150 | "12 और उससे कम" — ख़ुद किनारा | | पहली वयस्क उम्र | 13 | 300 | बच्चा किनारे के ठीक पार | | आख़िरी वयस्क उम्र | 64 | 300 | वरिष्ठ किनारे से नीचे | | पहली वरिष्ठ उम्र | 65 | 200 | "65 और ऊपर" — ख़ुद किनारा | | सबसे बड़ी मान्य उम्र | 120 | 200 | मान्य रेंज का शीर्ष | | रेंज से नीचे | -1 | ValueError | ठीक बाहर, निचली तरफ़ | | रेंज से ऊपर | 121 | ValueError | ठीक बाहर, ऊपरी तरफ़ |
5. टेस्ट नहीं किया गया: उम्र के लिए string या None — contract कहता है पूर्ण संख्या, और इसके अलावा कोई वादा नहीं करता। 12.5 जैसी दशमलव उम्र — वही बात। दूसरे सिनेमाघरों की कीमतें, या पायथन का <= — इन्हें टेस्ट करना हमारा काम नहीं। और उसी class में दूसरा सामान्य केस, जैसे 8 के बगल में 9 — वह कुछ नया नहीं पकड़ेगा।
6. जाँचें, check_cinema.py:
from cinema import ticket_price
def raises_value_error(age):
"""True if ticket_price raises ValueError for this age."""
try:
ticket_price(age)
except ValueError:
return True
return False
# One typical age from each class.
assert ticket_price(1) == 0
assert ticket_price(8) == 150
assert ticket_price(30) == 300
assert ticket_price(80) == 200
# Both sides of every edge between classes.
assert ticket_price(0) == 0
assert ticket_price(2) == 0
assert ticket_price(3) == 150
assert ticket_price(12) == 150
assert ticket_price(13) == 300
assert ticket_price(64) == 300
assert ticket_price(65) == 200
assert ticket_price(120) == 200
# Invalid input, just outside the valid range on each side.
assert raises_value_error(-1)
assert raises_value_error(121)
print("all 14 checks passed")$ python check_cinema.py
all 14 checks passedजान-बूझकर डाला गया बग: if age <= CHILD_UP_TO: बन जाता है if age < CHILD_UP_TO:। सिर्फ़ उम्र 12 की class बदलती है — वह फिसलकर वयस्क कीमत तक पहुँच जाती है — इसलिए सिर्फ़ 12 वाली जाँच फ़ेल होनी चाहिए:
$ python check_cinema.py
Traceback (most recent call last):
File "/home/you/cinema/check_cinema.py", line 23, in <module>
assert ticket_price(12) == 150
^^^^^^^^^^^^^^^^^^^^^^^
AssertionErrorलाइन 23 है assert ticket_price(12) == 150, जैसा अनुमान था।
ऐसा क्यों किया गया:
- सवाल पहले आए। "बच्चे" और "पेंशनभोगी" संख्याएँ नहीं हैं; पहली टेबल के हर जवाब ने एक शब्द को एक ऐसे किनारे में बदला जिसे टेस्ट किया जा सके। उनके बिना
12और65बस अंदाज़े होते। - फ़ंक्शन pure है। उम्र अंदर, कीमत बाहर, और कुछ नहीं — इसलिए हर जाँच एक लाइन की है, और दिन के किसी भी समय, किसी भी मशीन पर एक ही जवाब देती है।
- किनारों के लिए नाम वाले constants।
CHILD_UP_TO = 12एक ही जगह वही कहता है जो contract कहता है, और जाँचें वही संख्याएँ इस्तेमाल करती हैं। - असली काम boundary पंक्तियों ने किया। चारों सामान्य उम्रें — 1, 8, 30, 80 — बग के रहते भी पास होती हैं। सिर्फ़
12वाली पंक्ति, यानी ख़ुद किनारे ने, उसे पकड़ा। boundary values के पक्ष में पूरा तर्क एक ही रन में। - हर अपेक्षित मान contract से आया। कोई भी
ticket_priceचलाकर कॉपी नहीं किया गया, इसलिए कोड का ग़लत जवाब चुपके से जाँचों में नहीं घुस सकता। - यह पहली विफलता पर रुक गया, और सिर्फ़ लाइन बताई। आपको पता था कि कौन-सी जाँच फ़ेल हुई क्योंकि आपने अनुमान लगाया था — लेकिन इसने यह नहीं बताया कि उम्र 12 की असली कीमत 300 थी। यही वह कमी है जिसे अगला अध्याय भरता है।
Step 6 of 6
चुनौती — the chapter quiz
सरल से कठिन — दस प्रश्न, अंतिम वाले जानबूझकर चुनौतीपूर्ण बनाए गए हैं।
Sign in to take the quiz