पाइथन की हाइपोथिसिस लाइब्रेरी के साथ प्रॉपर्टी-बेस्ड टेस्टिंग की खोज करें। एज केस खोजने और अधिक मजबूत, विश्वसनीय सॉफ्टवेयर बनाने के लिए उदाहरण-आधारित टेस्ट से आगे बढ़ें।
यूनिट टेस्ट से परे: पाइथन की हाइपोथिसिस के साथ प्रॉपर्टी-बेस्ड टेस्टिंग का गहन विश्लेषण
सॉफ्टवेयर डेवलपमेंट की दुनिया में, टेस्टिंग क्वालिटी की नींव है। दशकों से, प्रमुख प्रतिमान उदाहरण-आधारित टेस्टिंग रहा है। हम सावधानीपूर्वक इनपुट तैयार करते हैं, अपेक्षित आउटपुट परिभाषित करते हैं, और यह सत्यापित करने के लिए असर्शन लिखते हैं कि हमारा कोड योजना के अनुसार व्यवहार करता है। unittest
और pytest
जैसे फ्रेमवर्क में पाया जाने वाला यह दृष्टिकोण शक्तिशाली और आवश्यक है। लेकिन क्या होगा अगर मैं आपको बताऊं कि एक पूरक दृष्टिकोण है जो उन बग्स को भी उजागर कर सकता है जिन्हें खोजने के बारे में आपने कभी सोचा भी नहीं होगा?
प्रॉपर्टी-बेस्ड टेस्टिंग की दुनिया में आपका स्वागत है, एक ऐसा प्रतिमान जो विशिष्ट उदाहरणों के परीक्षण से ध्यान हटाकर आपके कोड के सामान्य गुणों को सत्यापित करने पर केंद्रित है। और पाइथन इकोसिस्टम में, इस दृष्टिकोण का निर्विवाद चैंपियन हाइपोथिसिस नामक एक लाइब्रेरी है।
यह व्यापक गाइड आपको पूर्ण शुरुआत से प्रॉपर्टी-बेस्ड टेस्टिंग और हाइपोथिसिस के एक आत्मविश्वासी अभ्यासी तक ले जाएगा। हम मुख्य अवधारणाओं का पता लगाएंगे, व्यावहारिक उदाहरणों में गोता लगाएँगे, और सीखेंगे कि कैसे इस शक्तिशाली टूल को अपने दैनिक डेवलपमेंट वर्कफ़्लो में एकीकृत करके अधिक मजबूत, विश्वसनीय और बग-प्रतिरोधी सॉफ़्टवेयर बनाया जाए।
प्रॉपर्टी-बेस्ड टेस्टिंग क्या है? मानसिकता में एक बदलाव
हाइपोथिसिस को समझने के लिए, हमें पहले प्रॉपर्टी-बेस्ड टेस्टिंग के मौलिक विचार को समझना होगा। आइए इसकी तुलना पारंपरिक उदाहरण-आधारित टेस्टिंग से करें जिसे हम सभी जानते हैं।
उदाहरण-आधारित टेस्टिंग: परिचित मार्ग
कल्पना कीजिए कि आपने एक कस्टम सॉर्टिंग फ़ंक्शन, my_sort()
लिखा है। उदाहरण-आधारित टेस्टिंग के साथ, आपकी विचार प्रक्रिया होगी:
- "चलिए इसे एक सरल, क्रमबद्ध सूची के साथ टेस्ट करते हैं।" ->
assert my_sort([1, 2, 3]) == [1, 2, 3]
- "एक विपरीत-क्रम वाली सूची के बारे में क्या?" ->
assert my_sort([3, 2, 1]) == [1, 2, 3]
- "एक खाली सूची के बारे में क्या?" ->
assert my_sort([]) == []
- "डुप्लिकेट वाली सूची?" ->
assert my_sort([5, 1, 5, 2]) == [1, 2, 5, 5]
- "और ऋणात्मक संख्याओं वाली सूची?" ->
assert my_sort([-1, -5, 0]) == [-5, -1, 0]
यह प्रभावी है, लेकिन इसकी एक मौलिक सीमा है: आप केवल उन्हीं मामलों का परीक्षण कर रहे हैं जिनके बारे में आप सोच सकते हैं। आपके टेस्ट केवल आपकी कल्पना जितने ही अच्छे हैं। आप बहुत बड़ी संख्याओं, फ्लोटिंग-पॉइंट अशुद्धियों, विशिष्ट यूनिकोड वर्णों, या डेटा के जटिल संयोजनों से जुड़े एज केस को चूक सकते हैं जो अप्रत्याशित व्यवहार का कारण बनते हैं।
प्रॉपर्टी-बेस्ड टेस्टिंग: इनवेरिएंट्स में सोचना
प्रॉपर्टी-बेस्ड टेस्टिंग इस स्क्रिप्ट को पलट देती है। विशिष्ट उदाहरण प्रदान करने के बजाय, आप अपने फ़ंक्शन के गुणों, या इनवेरिएंट्स को परिभाषित करते हैं—ऐसे नियम जो किसी भी मान्य इनपुट के लिए सही होने चाहिए। हमारे my_sort()
फ़ंक्शन के लिए, ये गुण हो सकते हैं:
- आउटपुट सॉर्टेड है: संख्याओं की किसी भी सूची के लिए, आउटपुट सूची में प्रत्येक तत्व उसके बाद वाले तत्व से कम या उसके बराबर होता है।
- आउटपुट में इनपुट के समान ही तत्व होते हैं: सॉर्ट की गई सूची मूल सूची का केवल एक क्रमचय है; कोई भी तत्व जोड़ा या हटाया नहीं जाता है।
- फ़ंक्शन आइडमपोटेंट (idempotent) है: पहले से सॉर्ट की गई सूची को सॉर्ट करने से उसमें कोई बदलाव नहीं होना चाहिए। यानी,
my_sort(my_sort(some_list)) == my_sort(some_list)
।
इस दृष्टिकोण के साथ, आप टेस्ट डेटा नहीं लिख रहे हैं। आप नियम लिख रहे हैं। फिर आप हाइपोथिसिस जैसे फ्रेमवर्क को सैकड़ों या हजारों यादृच्छिक, विविध, और अक्सर पेचीदा इनपुट उत्पन्न करने देते हैं ताकि वे आपके गुणों को गलत साबित करने का प्रयास कर सकें। यदि उसे कोई ऐसा इनपुट मिलता है जो किसी गुण को तोड़ता है, तो उसने एक बग ढूंढ लिया है।
हाइपोथिसिस का परिचय: आपका स्वचालित टेस्ट डेटा जेनरेटर
हाइपोथिसिस पाइथन के लिए प्रमुख प्रॉपर्टी-बेस्ड टेस्टिंग लाइब्रेरी है। यह आपके द्वारा परिभाषित गुणों को लेता है और उन्हें चुनौती देने के लिए टेस्ट डेटा उत्पन्न करने का कठिन काम करता है। यह सिर्फ एक यादृच्छिक डेटा जेनरेटर नहीं है; यह एक बुद्धिमान और शक्तिशाली उपकरण है जिसे कुशलतापूर्वक बग्स खोजने के लिए डिज़ाइन किया गया है।
हाइपोथिसिस की मुख्य विशेषताएँ
- स्वचालित टेस्ट केस जनरेशन: आप अपने आवश्यक डेटा का *आकार* परिभाषित करते हैं (उदाहरण के लिए, "पूर्णांकों की एक सूची," "केवल अक्षरों वाली एक स्ट्रिंग," "भविष्य में एक डेटाइम"), और हाइपोथिसिस उस आकार के अनुरूप विभिन्न प्रकार के उदाहरण उत्पन्न करता है।
- इंटेलिजेंट श्रिंकिंग (Intelligent Shrinking): यह जादुई विशेषता है। जब हाइपोथिसिस को एक असफल टेस्ट केस मिलता है (उदाहरण के लिए, 50 जटिल संख्याओं की एक सूची जो आपके सॉर्ट फ़ंक्शन को क्रैश कर देती है), तो यह सिर्फ उस विशाल सूची की रिपोर्ट नहीं करता है। यह विफलता का कारण बनने वाले सबसे छोटे संभव उदाहरण को खोजने के लिए इनपुट को बुद्धिमानी और स्वचालित रूप से सरल बनाता है। 50-तत्वों की सूची के बजाय, यह रिपोर्ट कर सकता है कि विफलता केवल
[inf, nan]
के साथ होती है। यह डीबगिंग को अविश्वसनीय रूप से तेज और कुशल बनाता है। - सहज एकीकरण (Seamless Integration): हाइपोथिसिस
pytest
औरunittest
जैसे लोकप्रिय टेस्टिंग फ्रेमवर्क के साथ पूरी तरह से एकीकृत होता है। आप अपने मौजूदा उदाहरण-आधारित टेस्ट के साथ-साथ प्रॉपर्टी-बेस्ड टेस्ट जोड़ सकते हैं, बिना अपने वर्कफ़्लो को बदले। - स्ट्रेटेजीज़ की समृद्ध लाइब्रेरी: यह सरल पूर्णांक और स्ट्रिंग्स से लेकर जटिल, नेस्टेड डेटा संरचनाओं, टाइमज़ोन-अवेयर डेटाइम्स और यहां तक कि NumPy एरेज़ तक सब कुछ उत्पन्न करने के लिए अंतर्निहित "स्ट्रेटेजीज़" के एक विशाल संग्रह के साथ आता है।
- स्टेटफुल टेस्टिंग (Stateful Testing): अधिक जटिल प्रणालियों के लिए, हाइपोथिसिस स्टेट ट्रांज़िशन में बग्स खोजने के लिए क्रियाओं के अनुक्रमों का परीक्षण कर सकता है, जो कि उदाहरण-आधारित टेस्टिंग के साथ कुख्यात रूप से कठिन है।
आरंभ करना: आपका पहला हाइपोथिसिस टेस्ट
आइए व्यावहारिक बनें। हाइपोथिसिस को समझने का सबसे अच्छा तरीका इसे क्रिया में देखना है।
इंस्टॉलेशन
सबसे पहले, आपको हाइपोथिसिस और अपनी पसंद का टेस्ट रनर (हम pytest
का उपयोग करेंगे) इंस्टॉल करना होगा। यह इतना सरल है:
pip install pytest hypothesis
एक सरल उदाहरण: एक एब्सोल्यूट वैल्यू फ़ंक्शन
आइए एक सरल फ़ंक्शन पर विचार करें जिसे किसी संख्या का निरपेक्ष मान (absolute value) परिकलित करना है। एक थोड़ा buggy कार्यान्वयन इस तरह दिख सकता है:
# `my_math.py` नामक फ़ाइल में def custom_abs(x): """एब्सोल्यूट वैल्यू फ़ंक्शन का एक कस्टम कार्यान्वयन।""" if x < 0: return -x return x
अब, आइए एक टेस्ट फ़ाइल, test_my_math.py
लिखें। सबसे पहले, पारंपरिक pytest
दृष्टिकोण:
# test_my_math.py (उदाहरण-आधारित) def test_abs_positive(): assert custom_abs(5) == 5 def test_abs_negative(): assert custom_abs(-5) == 5 def test_abs_zero(): assert custom_abs(0) == 0
ये टेस्ट पास हो जाते हैं। इन उदाहरणों के आधार पर हमारा फ़ंक्शन सही लगता है। लेकिन अब, आइए हाइपोथिसिस के साथ एक प्रॉपर्टी-बेस्ड टेस्ट लिखें। एब्सोल्यूट वैल्यू फ़ंक्शन का एक मुख्य गुण क्या है? परिणाम कभी भी ऋणात्मक नहीं होना चाहिए।
# test_my_math.py (हाइपोथिसिस के साथ प्रॉपर्टी-बेस्ड) from hypothesis import given from hypothesis import strategies as st from my_math import custom_abs @given(st.integers()) def test_abs_property_is_non_negative(x): """प्रॉपर्टी: किसी भी पूर्णांक का निरपेक्ष मान हमेशा >= 0 होता है।""" assert custom_abs(x) >= 0
आइए इसे तोड़कर समझते हैं:
from hypothesis import given, strategies as st
: हम आवश्यक घटकों को आयात करते हैं।given
एक डेकोरेटर है जो एक नियमित टेस्ट फ़ंक्शन को प्रॉपर्टी-बेस्ड टेस्ट में बदल देता है।strategies
वह मॉड्यूल है जहां हम अपने डेटा जेनरेटर पाते हैं।@given(st.integers())
: यह टेस्ट का मूल है।@given
डेकोरेटर हाइपोथिसिस को इस टेस्ट फ़ंक्शन को कई बार चलाने के लिए कहता है। प्रत्येक रन के लिए, यह प्रदान की गई स्ट्रेटेजी,st.integers()
, का उपयोग करके एक मान उत्पन्न करेगा और इसे हमारे टेस्ट फ़ंक्शन में तर्कx
के रूप में पास करेगा।assert custom_abs(x) >= 0
: यह हमारी प्रॉपर्टी है। हम यह दावा करते हैं कि हाइपोथिसिस जो भी पूर्णांकx
सोचे, हमारे फ़ंक्शन का परिणाम शून्य से बड़ा या बराबर होना चाहिए।
जब आप इसे pytest
के साथ चलाते हैं, तो यह कई मानों के लिए संभवतः पास हो जाएगा। हाइपोथिसिस 0, -1, 1, बड़ी धनात्मक संख्याएँ, बड़ी ऋणात्मक संख्याएँ, और बहुत कुछ आज़माएगा। हमारा सरल फ़ंक्शन इन सभी को सही ढंग से संभालता है। अब, आइए यह देखने के लिए एक अलग स्ट्रेटेजी का प्रयास करें कि क्या हम कोई कमजोरी ढूंढ सकते हैं।
# चलिए फ्लोटिंग पॉइंट नंबरों के साथ टेस्ट करते हैं @given(st.floats()) def test_abs_floats_property(x): assert custom_abs(x) >= 0
यदि आप इसे चलाते हैं, तो हाइपोथिसिस जल्दी से एक असफल मामला ढूंढ लेगा!
Falsifying example: test_abs_floats_property(x=nan) ... assert custom_abs(nan) >= 0 AssertionError: assert nan >= 0
हाइपोथिसिस ने पाया कि हमारा फ़ंक्शन, जब float('nan')
(Not a Number) दिया जाता है, तो nan
लौटाता है। nan >= 0
का दावा गलत है। हमने अभी-अभी एक सूक्ष्म बग पाया है जिसे हम शायद मैन्युअल रूप से टेस्ट करने के बारे में नहीं सोचते। हम इस मामले को संभालने के लिए अपने फ़ंक्शन को ठीक कर सकते हैं, शायद ValueError
उठाकर या एक विशिष्ट मान लौटाकर।
और भी बेहतर, क्या होता अगर बग एक बहुत ही विशिष्ट फ्लोट के साथ होता? हाइपोथिसिस का श्रिंकर एक बड़ी, जटिल असफल संख्या को लेकर उसे सबसे सरल संभव संस्करण में बदल देता जो अभी भी बग को ट्रिगर करता है।
स्ट्रेटेजीज़ की शक्ति: अपना टेस्ट डेटा तैयार करना
स्ट्रेटेजीज़ हाइपोथिसिस का हृदय हैं। वे डेटा उत्पन्न करने के लिए रेसिपी हैं। लाइब्रेरी में अंतर्निहित स्ट्रेटेजीज़ की एक विशाल श्रृंखला शामिल है, और आप उन्हें अपनी कल्पना के किसी भी डेटा संरचना को उत्पन्न करने के लिए संयोजित और अनुकूलित कर सकते हैं।
सामान्य अंतर्निहित स्ट्रेटेजीज़
- संख्यात्मक:
st.integers(min_value=0, max_value=1000)
: पूर्णांक उत्पन्न करता है, वैकल्पिक रूप से एक विशिष्ट सीमा के भीतर।st.floats(min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False)
: फ्लोट्स उत्पन्न करता है, विशेष मानों पर बारीक नियंत्रण के साथ।st.fractions()
,st.decimals()
- टेक्स्ट:
st.text(min_size=1, max_size=50)
: एक निश्चित लंबाई के यूनिकोड स्ट्रिंग्स उत्पन्न करता है।st.text(alphabet='abcdef0123456789')
: एक विशिष्ट वर्ण सेट से स्ट्रिंग्स उत्पन्न करता है (उदाहरण के लिए, हेक्स कोड के लिए)।st.characters()
: व्यक्तिगत वर्ण उत्पन्न करता है।
- संग्रह (Collections):
st.lists(st.integers(), min_size=1)
: सूचियाँ उत्पन्न करता है जहाँ प्रत्येक तत्व एक पूर्णांक है। ध्यान दें कि हम एक और स्ट्रेटेजी को एक तर्क के रूप में कैसे पास करते हैं! इसे कंपोजीशन कहते हैं।st.tuples(st.text(), st.booleans())
: एक निश्चित संरचना के साथ टपल्स उत्पन्न करता है।st.sets(st.integers())
st.dictionaries(keys=st.text(), values=st.integers())
: निर्दिष्ट कुंजी और मान प्रकारों के साथ शब्दकोश उत्पन्न करता है।
- सामयिक (Temporal):
st.dates()
,st.times()
,st.datetimes()
,st.timedeltas()
। इन्हें टाइमज़ोन-अवेयर बनाया जा सकता है।
- विविध:
st.booleans()
:True
याFalse
उत्पन्न करता है।st.just('constant_value')
: हमेशा एक ही मान उत्पन्न करता है। जटिल स्ट्रेटेजीज़ को बनाने के लिए उपयोगी।st.one_of(st.integers(), st.text())
: प्रदान की गई स्ट्रेटेजीज़ में से किसी एक से एक मान उत्पन्न करता है।st.none()
: केवलNone
उत्पन्न करता है।
स्ट्रेटेजीज़ को जोड़ना और बदलना
हाइपोथिसिस की असली शक्ति सरल स्ट्रेटेजीज़ से जटिल स्ट्रेटेजीज़ बनाने की क्षमता में निहित है।
.map()
का उपयोग करना
.map()
विधि आपको एक स्ट्रेटेजी से एक मान लेने और उसे किसी और चीज़ में बदलने की सुविधा देती है। यह आपके कस्टम क्लास की ऑब्जेक्ट बनाने के लिए एकदम सही है।
# एक सरल डेटा क्लास from dataclasses import dataclass @dataclass class User: user_id: int username: str # User ऑब्जेक्ट्स उत्पन्न करने के लिए एक स्ट्रेटेजी user_strategy = st.builds( User, user_id=st.integers(min_value=1), username=st.text(min_size=3, alphabet='abcdefghijklmnopqrstuvwxyz') ) @given(user=user_strategy) def test_user_creation(user): assert isinstance(user, User) assert user.user_id > 0 assert user.username.isalpha()
.filter()
और assume()
का उपयोग करना
कभी-कभी आपको कुछ उत्पन्न मानों को अस्वीकार करने की आवश्यकता होती है। उदाहरण के लिए, आपको पूर्णांकों की एक सूची की आवश्यकता हो सकती है जहाँ योग शून्य न हो। आप .filter()
का उपयोग कर सकते हैं:
st.lists(st.integers()).filter(lambda x: sum(x) != 0)
हालांकि, .filter()
का उपयोग करना अक्षम हो सकता है। यदि शर्त अक्सर गलत होती है, तो हाइपोथिसिस एक वैध उदाहरण उत्पन्न करने में लंबा समय लगा सकता है। एक बेहतर दृष्टिकोण अक्सर आपके टेस्ट फ़ंक्शन के अंदर assume()
का उपयोग करना होता है:
from hypothesis import assume @given(st.lists(st.integers())) def test_something_with_non_zero_sum_list(numbers): assume(sum(numbers) != 0) # ... आपका टेस्ट लॉजिक यहाँ ...
assume()
हाइपोथिसिस को बताता है: "यदि यह शर्त पूरी नहीं होती है, तो बस इस उदाहरण को छोड़ दें और एक नया प्रयास करें।" यह आपके टेस्ट डेटा को बाधित करने का एक अधिक सीधा और अक्सर अधिक प्रदर्शनकारी तरीका है।
st.composite()
का उपयोग करना
वास्तव में जटिल डेटा जनरेशन के लिए जहाँ एक उत्पन्न मान दूसरे पर निर्भर करता है, st.composite()
वह टूल है जिसकी आपको आवश्यकता है। यह आपको एक फ़ंक्शन लिखने की अनुमति देता है जो एक विशेष draw
फ़ंक्शन को एक तर्क के रूप में लेता है, जिसका उपयोग आप अन्य स्ट्रेटेजीज़ से चरण-दर-चरण मान निकालने के लिए कर सकते हैं।
एक क्लासिक उदाहरण एक सूची और उस सूची में एक वैध इंडेक्स उत्पन्न करना है।
@st.composite def list_and_index(draw): # सबसे पहले, एक गैर-खाली सूची ड्रा करें my_list = draw(st.lists(st.integers(), min_size=1)) # फिर, एक इंडेक्स ड्रा करें जो उस सूची के लिए मान्य होने की गारंटी है index = draw(st.integers(min_value=0, max_value=len(my_list) - 1)) return (my_list, index) @given(data=list_and_index()) def test_list_access(data): my_list, index = data # यह एक्सेस सुरक्षित होने की गारंटी है क्योंकि हमने स्ट्रेटेजी कैसे बनाई है element = my_list[index] assert element is not None # एक सरल दावा
हाइपोथिसिस का व्यावहारिक उपयोग: वास्तविक दुनिया के परिदृश्य
आइए इन अवधारणाओं को अधिक यथार्थवादी समस्याओं पर लागू करें जिनका सामना सॉफ्टवेयर डेवलपर्स हर दिन करते हैं।
परिदृश्य 1: डेटा सीरियलाइज़ेशन फ़ंक्शन का परीक्षण
एक ऐसे फ़ंक्शन की कल्पना करें जो एक उपयोगकर्ता प्रोफ़ाइल (एक शब्दकोश) को एक URL-सुरक्षित स्ट्रिंग में सीरियलाइज़ करता है और दूसरा जो इसे डीसीरियलाइज़ करता है। एक प्रमुख गुण यह है कि प्रक्रिया पूरी तरह से प्रतिवर्ती (reversible) होनी चाहिए।
import json import base64 def serialize_profile(data: dict) -> str: """एक शब्दकोश को URL-सुरक्षित base64 स्ट्रिंग में सीरियलाइज़ करता है।""" json_string = json.dumps(data) return base64.urlsafe_b64encode(json_string.encode('utf-8')).decode('utf-8') def deserialize_profile(encoded_str: str) -> dict: """एक स्ट्रिंग को वापस एक शब्दकोश में डीसीरियलाइज़ करता है।""" json_string = base64.urlsafe_b64decode(encoded_str.encode('utf-8')).decode('utf-8') return json.loads(json_string) # अब टेस्ट के लिए # हमें एक स्ट्रेटेजी चाहिए जो JSON-संगत शब्दकोश उत्पन्न करे json_dictionaries = st.dictionaries( keys=st.text(), values=st.recursive(st.none() | st.booleans() | st.floats(allow_nan=False) | st.text(), lambda children: st.lists(children) | st.dictionaries(st.text(), children), max_leaves=10) ) @given(profile=json_dictionaries) def test_serialization_roundtrip(profile): """प्रॉपर्टी: एक एन्कोडेड प्रोफ़ाइल को डीसीरियलाइज़ करने पर मूल प्रोफ़ाइल वापस आनी चाहिए।""" encoded = serialize_profile(profile) decoded = deserialize_profile(encoded) assert profile == decoded
यह एकल टेस्ट हमारे फ़ंक्शंस पर विभिन्न प्रकार के डेटा के साथ भारी प्रहार करेगा: खाली शब्दकोश, नेस्टेड सूचियों वाले शब्दकोश, यूनिकोड वर्णों वाले शब्दकोश, अजीब कुंजियों वाले शब्दकोश, और बहुत कुछ। यह कुछ मैन्युअल उदाहरण लिखने की तुलना में कहीं अधिक संपूर्ण है।
परिदृश्य 2: एक सॉर्टिंग एल्गोरिथ्म का परीक्षण
आइए हम अपने सॉर्टिंग उदाहरण पर फिर से विचार करें। यहाँ बताया गया है कि आप उन गुणों का परीक्षण कैसे करेंगे जिन्हें हमने पहले परिभाषित किया था।
from collections import Counter def my_buggy_sort(numbers): # आइए एक सूक्ष्म बग पेश करें: यह डुप्लिकेट को हटा देता है return sorted(list(set(numbers))) @given(st.lists(st.integers())) def test_sorting_properties(numbers): sorted_list = my_buggy_sort(numbers) # प्रॉपर्टी 1: आउटपुट सॉर्ट किया गया है for i in range(len(sorted_list) - 1): assert sorted_list[i] <= sorted_list[i+1] # प्रॉपर्टी 2: तत्व समान हैं (यह बग ढूंढेगा) assert Counter(numbers) == Counter(sorted_list) # प्रॉपर्टी 3: फ़ंक्शन आइडमपोटेंट है assert my_buggy_sort(sorted_list) == sorted_list
जब आप इस टेस्ट को चलाते हैं, तो हाइपोथिसिस प्रॉपर्टी 2 के लिए जल्दी से एक असफल उदाहरण ढूंढेगा, जैसे कि numbers=[0, 0]
। हमारा फ़ंक्शन [0]
लौटाता है, और Counter([0, 0])
Counter([0])
के बराबर नहीं है। श्रिंकर यह सुनिश्चित करेगा कि असफल उदाहरण यथासंभव सरल हो, जिससे बग का कारण तुरंत स्पष्ट हो जाए।
परिदृश्य 3: स्टेटफुल टेस्टिंग
आंतरिक स्थिति वाली वस्तुओं के लिए जो समय के साथ बदलती हैं (जैसे डेटाबेस कनेक्शन, शॉपिंग कार्ट, या कैश), बग्स ढूंढना अविश्वसनीय रूप से कठिन हो सकता है। किसी दोष को ट्रिगर करने के लिए संचालन के एक विशिष्ट अनुक्रम की आवश्यकता हो सकती है। हाइपोथिसिस ठीक इसी उद्देश्य के लिए `RuleBasedStateMachine` प्रदान करता है।
एक इन-मेमोरी की-वैल्यू स्टोर के लिए एक सरल एपीआई की कल्पना करें:
class SimpleKeyValueStore: def __init__(self): self._data = {} def set(self, key, value): self._data[key] = value def get(self, key): return self._data.get(key) def delete(self, key): if key in self._data: del self._data[key] def size(self): return len(self._data)
हम इसके व्यवहार को मॉडल कर सकते हैं और इसे एक स्टेट मशीन के साथ टेस्ट कर सकते हैं:
from hypothesis.stateful import RuleBasedStateMachine, rule, Bundle class KeyValueStoreMachine(RuleBasedStateMachine): def __init__(self): super().__init__() self.model = {} self.sut = SimpleKeyValueStore() # Bundle() का उपयोग नियमों के बीच डेटा पास करने के लिए किया जाता है keys = Bundle('keys') @rule(target=keys, key=st.text(), value=st.integers()) def set_key(self, key, value): self.model[key] = value self.sut.set(key, value) return key @rule(key=keys) def delete_key(self, key): del self.model[key] self.sut.delete(key) @rule(key=st.text()) def get_key(self, key): model_val = self.model.get(key) sut_val = self.sut.get(key) assert model_val == sut_val @rule() def check_size(self): assert len(self.model) == self.sut.size() # टेस्ट चलाने के लिए, आप बस मशीन और unittest.TestCase से सबक्लास करते हैं # pytest में, आप बस टेस्ट को मशीन क्लास को असाइन कर सकते हैं TestKeyValueStore = KeyValueStoreMachine.TestCase
हाइपोथिसिस अब set_key
, delete_key
, get_key
, और check_size
के यादृच्छिक अनुक्रमों को निष्पादित करेगा, अथक रूप से एक ऐसा अनुक्रम खोजने की कोशिश करेगा जो किसी एक दावे को विफल कर दे। यह जांच करेगा कि क्या एक हटाई गई कुंजी प्राप्त करना सही ढंग से व्यवहार करता है, क्या आकार कई सेट और डिलीट के बाद सुसंगत है, और कई अन्य परिदृश्य जिन्हें आप मैन्युअल रूप से टेस्ट करने के बारे में नहीं सोच सकते हैं।
सर्वोत्तम अभ्यास और उन्नत युक्तियाँ
- उदाहरण डेटाबेस: हाइपोथिसिस स्मार्ट है। जब यह एक बग ढूंढता है, तो यह असफल उदाहरण को एक स्थानीय निर्देशिका (
.hypothesis/
) में सहेजता है। अगली बार जब आप अपने टेस्ट चलाते हैं, तो यह सबसे पहले उस असफल उदाहरण को फिर से चलाएगा, जिससे आपको तुरंत प्रतिक्रिया मिलेगी कि बग अभी भी मौजूद है। एक बार जब आप इसे ठीक कर लेते हैं, तो उदाहरण फिर से नहीं चलाया जाता है। @settings
के साथ टेस्ट निष्पादन को नियंत्रित करना: आप@settings
डेकोरेटर का उपयोग करके टेस्ट रन के कई पहलुओं को नियंत्रित कर सकते हैं। आप उदाहरणों की संख्या बढ़ा सकते हैं, एक एकल उदाहरण के चलने की समय सीमा निर्धारित कर सकते हैं (अनंत लूप को पकड़ने के लिए), और कुछ स्वास्थ्य जांच बंद कर सकते हैं।@settings(max_examples=500, deadline=1000) # 500 उदाहरण चलाएं, 1-सेकंड की समय सीमा @given(...) ...
- विफलताओं को पुन: उत्पन्न करना: प्रत्येक हाइपोथिसिस रन एक सीड मान प्रिंट करता है (उदाहरण के लिए,
@reproduce_failure('version', 'seed')
)। यदि एक CI सर्वर एक बग ढूंढता है जिसे आप स्थानीय रूप से पुन: उत्पन्न नहीं कर सकते हैं, तो आप इस डेकोरेटर का उपयोग प्रदान किए गए सीड के साथ कर सकते हैं ताकि हाइपोथिसिस को उदाहरणों का ठीक वही अनुक्रम चलाने के लिए बाध्य किया जा सके। - CI/CD के साथ एकीकरण: हाइपोथिसिस किसी भी निरंतर एकीकरण पाइपलाइन के लिए एक आदर्श फिट है। उत्पादन में पहुंचने से पहले अस्पष्ट बग्स को खोजने की इसकी क्षमता इसे एक अमूल्य सुरक्षा जाल बनाती है।
मानसिकता में बदलाव: प्रॉपर्टीज़ के संदर्भ में सोचना
हाइपोथिसिस को अपनाना केवल एक नई लाइब्रेरी सीखने से कहीं अधिक है; यह आपके कोड की शुद्धता के बारे में सोचने का एक नया तरीका अपनाने के बारे में है। "मुझे कौन से इनपुट का परीक्षण करना चाहिए?" पूछने के बजाय, आप पूछना शुरू करते हैं, "इस कोड के बारे में सार्वभौमिक सत्य क्या हैं?"
प्रॉपर्टीज़ की पहचान करने का प्रयास करते समय आपको मार्गदर्शन करने के लिए यहां कुछ प्रश्न दिए गए हैं:
- क्या कोई विपरीत ऑपरेशन है? (उदाहरण के लिए, सीरियलाइज़/डीसीरियलाइज़, एन्क्रिप्ट/डिक्रिप्ट, कंप्रेस/डीकंप्रेस)। प्रॉपर्टी यह है कि ऑपरेशन और उसके विपरीत का प्रदर्शन करने पर मूल इनपुट मिलना चाहिए।
- क्या ऑपरेशन आइडमपोटेंट है? (उदाहरण के लिए,
abs(abs(x)) == abs(x)
)। फ़ंक्शन को एक से अधिक बार लागू करने पर वही परिणाम मिलना चाहिए जो इसे एक बार लागू करने पर मिलता है। - क्या एक ही परिणाम की गणना करने का कोई अलग, सरल तरीका है? आप यह परीक्षण कर सकते हैं कि आपका जटिल, अनुकूलित फ़ंक्शन एक सरल, स्पष्ट रूप से सही संस्करण के समान ही आउटपुट उत्पन्न करता है (उदाहरण के लिए, अपने फैंसी सॉर्ट को पाइथन के अंतर्निहित
sorted()
के विरुद्ध परीक्षण करना)। - आउटपुट के बारे में हमेशा क्या सच होना चाहिए? (उदाहरण के लिए, एक `find_prime_factors` फ़ंक्शन के आउटपुट में केवल अभाज्य संख्याएँ होनी चाहिए, और उनका गुणनफल इनपुट के बराबर होना चाहिए)।
- स्टेट कैसे बदलता है? (स्टेटफुल टेस्टिंग के लिए) किसी भी वैध ऑपरेशन के बाद कौन से इनवेरिएंट्स बनाए रखने चाहिए? (उदाहरण के लिए, शॉपिंग कार्ट में वस्तुओं की संख्या कभी भी ऋणात्मक नहीं हो सकती है)।
निष्कर्ष: आत्मविश्वास का एक नया स्तर
हाइपोथिसिस के साथ प्रॉपर्टी-बेस्ड टेस्टिंग उदाहरण-आधारित टेस्टिंग की जगह नहीं लेती है। आपको अभी भी महत्वपूर्ण व्यावसायिक तर्क और अच्छी तरह से समझी गई आवश्यकताओं के लिए विशिष्ट, हाथ से लिखे गए टेस्ट की आवश्यकता है (उदाहरण के लिए, "देश X के एक उपयोगकर्ता को मूल्य Y दिखना चाहिए")।
हाइपोथिसिस जो प्रदान करता है वह आपके कोड के व्यवहार का पता लगाने और अप्रत्याशित एज केस से बचाने का एक शक्तिशाली, स्वचालित तरीका है। यह एक अथक भागीदार के रूप में कार्य करता है, हजारों टेस्ट उत्पन्न करता है जो किसी भी इंसान द्वारा यथार्थवादी रूप से लिखे जा सकने वाले से अधिक विविध और पेचीदा होते हैं। अपने कोड के मौलिक गुणों को परिभाषित करके, आप एक मजबूत विनिर्देश बनाते हैं जिसके विरुद्ध हाइपोथिसिस परीक्षण कर सकता है, जो आपको आपके सॉफ़्टवेयर में आत्मविश्वास का एक नया स्तर देता है।
अगली बार जब आप कोई फ़ंक्शन लिखें, तो उदाहरणों से परे सोचने के लिए एक क्षण लें। अपने आप से पूछें, "नियम क्या हैं? हमेशा क्या सच होना चाहिए?" फिर, हाइपोथिसिस को उन्हें तोड़ने की कोशिश करने का कठिन काम करने दें। आप यह जानकर आश्चर्यचकित होंगे कि यह क्या पाता है, और आपका कोड इसके लिए बेहतर होगा।