Fixture — टेस्ट की तैयारी एक जगह
एक ही setup बार-बार न लिखें: @pytest.fixture से एक बार लिखें, नाम से माँगें, हर टेस्ट में नई कॉपी पाएँ, और yield से सफ़ाई करें — टेस्ट fail होने पर भी।
- 1समस्या
- 2समझें
- 3उदाहरण
- 4अनुमान
- 5स्वयं करें
- 6चुनौती
वह समस्या जिसे हम हल कर रहे हैं
यह रहा एक छोटा-सा शॉपिंग कार्ट, cart.py:
class Cart:
def __init__(self):
self.lines = []
def add(self, name, price, quantity=1):
if quantity < 1:
raise ValueError(f"quantity must be at least 1: {quantity}")
self.lines.append((name, price, quantity))
def total(self):
return sum(price * quantity for _, price, quantity in self.lines)
def count(self):
return sum(quantity for _, _, quantity in self.lines)और इसके लिए तीन test, test_cart.py:
from cart import Cart
def test_total():
cart = Cart()
cart.add("pen", 15.0, 3)
cart.add("bag", 850.0)
assert cart.total() == 895.0
def test_count():
cart = Cart()
cart.add("pen", 15.0, 3)
cart.add("bag", 850.0)
assert cart.count() == 4
def test_add_one_more():
cart = Cart()
cart.add("pen", 15.0, 3)
cart.add("bag", 850.0)
cart.add("notebook", 60.0, 2)
assert cart.total() == 1015.0pytest -q
... [100%]
3 passed in 0.01sये पास हो जाते हैं, लेकिन इन्हें पढ़ना थकाऊ है। तीन test, setup की नौ लाइनें, और सिर्फ़ तीन लाइनें जो वास्तव में कुछ जाँचती हैं। setup हर test में एक जैसा है, इसलिए वह एक लाइन जो अलग है — वही लाइन जिसके लिए test मौजूद है — उसके नीचे दब जाती है।
इसकी क़ीमत आगे चलकर भी चुकानी पड़ती है। जब Cart() को किसी argument की ज़रूरत पड़ने लगेगी, तो आपको उसे तीन जगह बदलना होगा, या तीस जगह। पिछले अध्याय में parametrize ने test के inputs में दोहराव को हटाया था। यह अध्याय उसके setup में दोहराव को हटाता है।
इस अध्याय के अंत में आप कर पाएंगे
@pytest.fixtureसे एक fixture लिखना और उसे test के parameter के रूप में नाम देकर माँगना- यह समझाना कि हर test को अपनी नई कॉपी क्यों मिलती है, और एक लिस्ट से इसे दिखाना
- एक fixture को दूसरे fixture से बनाना
- एक
yieldfixture लिखना जो अपने बाद सफ़ाई करे, और setup व teardown के क्रम का अनुमान लगाना --setup-showसे fixtures को बनते हुए देखनाfixture 'x' not foundऔर "called directly" एरर को पढ़ना- fixture और एक साधारण helper function के बीच चुनाव करना
ज़रूरी शर्तें: `parametrize` से एक test को कई cases पर चलाना।
टेस्ट लिखने से पहले
fixtures ऐसी चीज़ नहीं हैं जिन्हें आप बाद में tests में जोड़ते हैं। वे एक योजना से अपने-आप निकलते हैं, और योजना पहले आती है।
क्या वादा किया गया है। Cart तीन चीज़ों का वादा करता है: add() एक line दर्ज करता है और 1 से कम quantity को ValueError के साथ ठुकरा देता है; total() price × quantity का योग है; count() quantities का योग है। कोई फ़ाइल नहीं, कोई network नहीं, कोई global state नहीं — अभी तक ऐसा कुछ नहीं जिसे पलटने की ज़रूरत हो।
पहले से क्या मौजूद होना चाहिए। वही जो हर अध्याय में: एक virtual environment जिसमें pytest इंस्टॉल हो, और cart.py उस फ़ोल्डर से import हो सके जिसमें आप pytest चलाते हैं (test फ़ाइल को उसी के बगल में रखें)।
कौन-से cases। उन्हें लिख लें, और एक ऐसा कॉलम जोड़ें जिसे लोग अक्सर छोड़ देते हैं — वह स्थिति जिससे test शुरू होता है:
| Case | शुरुआती स्थिति | क्रिया | अपेक्षित | |---|---|---|---| | सामान्य total | pen × 3, bag × 1 | total() | 895.0 | | आइटम की गिनती | pen × 3, bag × 1 | count() | 4 | | एक और line | pen × 3, bag × 1 | add("notebook", 60.0, 2) | total() का मान 1015.0 | | किनारे का case: कार्ट ख़ाली | ख़ाली कार्ट | total() | 0 | | अमान्य quantity | ख़ाली कार्ट | add("pen", 15.0, 0) | ValueError |
दूसरे कॉलम को ऊपर से नीचे पढ़ें। तीन rows एक ही भरे हुए कार्ट से शुरू होती हैं, दो एक ख़ाली कार्ट से। उस कॉलम में दोहराई गई हर वैल्यू एक fixture है जो लिखे जाने का इंतज़ार कर रहा है — यहाँ, empty_cart और उसके ऊपर बना एक cart। और जो row ऐसी स्थिति से शुरू होती है जिसे कोई और साझा नहीं करता, उसे fixture की ज़रूरत ही नहीं; वह अपनी स्थिति खुद बनाती है।
हर शुरुआती स्थिति से एक और सवाल पूछें: क्या इसे बनाने से test के बाहर कुछ बदलता है? एक नया Cart कुछ नहीं बदलता। किसी global डिक्शनरी में रजिस्टर किया गया discount code, खोला गया connection, लिखी गई फ़ाइल — ये बदलते हैं, और यही आपको बताता है कि fixture को सफ़ाई की ज़रूरत है (yield, इस अध्याय में आगे)।
क्या test नहीं करना है। ऐसे tests न लिखें जो साबित करें कि pytest हर test के लिए नया fixture बनाता है; यह pytest का वादा है, और यह अध्याय इसे एक बार दिखाता है ताकि आप उस पर भरोसा कर सकें। लेकिन अपनी सफ़ाई को ज़रूर test करें जब वह global state को वापस ठीक करती है — वह कोड आपका है, और वह ग़लत हो सकता है।
बाकी अध्याय इस योजना को कोड में बदलता है।
पहला fixture
setup को एक function में ले जाएँ, और उसके ऊपर @pytest.fixture लगाएँ:
import pytest
from cart import Cart
@pytest.fixture
def cart():
c = Cart()
c.add("pen", 15.0, 3)
c.add("bag", 850.0)
return c
def test_total(cart):
assert cart.total() == 895.0
def test_count(cart):
assert cart.count() == 4
def test_add_one_more(cart):
cart.add("notebook", 60.0, 2)
assert cart.total() == 1015.0pytest -q
... [100%]
3 passed in 0.01stests में कहीं भी cart() को call नहीं किया गया है। हर test में बस cart नाम का एक parameter है, और पूरी माँग बस इतनी ही है।
किसी test को चलाने से पहले pytest उसके parameters के नाम देखता है। हर नाम के लिए वह उसी नाम का fixture ढूँढता है, उसे चलाता है, और जो कुछ वह लौटाता है उसे argument के रूप में पास कर देता है। test बताता है कि उसे क्या चाहिए; वह उसे खुद नहीं बनाता। इस व्यवस्था का एक नाम है — dependency injection — लेकिन इसका तंत्र इससे ज़्यादा कुछ नहीं कि "parameter के नाम को fixture के नाम से मिलाओ"।
अब test एक लाइन में अपना इरादा बताता है। और setup एक ही जगह रहता है, इसलिए Cart() में बदलाव का मतलब सिर्फ़ एक function में बदलाव है।
हर test को एक नया fixture मिलता है
test_add_one_more कार्ट में एक notebook जोड़ता है। क्या उसके बाद चलने वाले tests को ऐसा कार्ट दिखा जिसमें notebook था? नहीं — और इसे साफ़ तौर पर देखना ज़रूरी है, क्योंकि यही वह गुण है जो fixtures को सुरक्षित बनाता है।
पहले, बिना fixture वाला संस्करण। module के ऊपर एक लिस्ट, जिसे दो tests साझा करते हैं:
ITEMS = ["pen", "bag"]
def test_add_item():
ITEMS.append("notebook")
assert len(ITEMS) == 3
def test_starts_with_two():
assert len(ITEMS) == 2pytest -q
.F [100%]
=================================== FAILURES ===================================
_____________________________ test_starts_with_two _____________________________
def test_starts_with_two():
> assert len(ITEMS) == 2
E AssertionError: assert 3 == 2
E + where 3 = len(['pen', 'bag', 'notebook'])
test_shared.py:10: AssertionError
=========================== short test summary info ============================
FAILED test_shared.py::test_starts_with_two - AssertionError: assert 3 == 2
1 failed, 1 passed in 0.01sदूसरा test सही है; वह इसलिए फ़ेल हुआ क्योंकि पहले test ने कुछ किया था। उसे अकेले चलाएँ तो वह पास हो जाता है:
pytest -q test_shared.py::test_starts_with_two
. [100%]
1 passed in 0.01sऐसा test जिसका परिणाम इस पर निर्भर करे कि उससे पहले कौन-से tests चले, किसी test suite के सबसे महँगे bugs में से एक है, क्योंकि वह सिर्फ़ कभी-कभी दिखाई देता है।
अब वही दो tests, लेकिन लिस्ट एक fixture से आ रही है। fixture के अंदर एक print दिखाता है कि वह कब चलता है; -s pytest को बताता है कि प्रिंट किया गया आउटपुट capture न करे, ताकि हम उसे देख सकें:
import pytest
@pytest.fixture
def items():
print("building a new list")
return ["pen", "bag"]
def test_add_item(items):
items.append("notebook")
assert len(items) == 3
def test_starts_with_two(items):
assert len(items) == 2pytest -q -s
building a new list
.building a new list
.
2 passed in 0.01s"building a new list" दो बार दिखाई देता है। fixture function हर उस test के लिए एक बार चला जिसने उसे माँगा, और हर test को ऐसी लिस्ट मिली जिसे किसी और ने छुआ तक नहीं था। यही डिफ़ॉल्ट है, और इसका एक नाम है, function scope: fixture की वैल्यू उतनी ही देर ज़िंदा रहती है जितनी देर एक test function। आठवाँ अध्याय दिखाता है कि इसे कैसे बदला जाए, और आमतौर पर आपको ऐसा क्यों नहीं करना चाहिए।
fixtures भी fixtures का उपयोग कर सकते हैं
एक fixture किसी दूसरे fixture को ठीक उसी तरह माँग सकता है जैसे कोई test — उसे parameter के रूप में नाम देकर:
import pytest
from cart import Cart
@pytest.fixture
def empty_cart():
return Cart()
@pytest.fixture
def cart(empty_cart):
empty_cart.add("pen", 15.0, 3)
empty_cart.add("bag", 850.0)
return empty_cart
def test_new_cart_is_empty(empty_cart):
assert empty_cart.total() == 0
def test_total(cart):
assert cart.total() == 895.0pytest -q
.. [100%]
2 passed in 0.01scart को empty_cart के ऊपर बनाया गया है, इसलिए कोई test तैयारी का वह स्तर माँग सकता है जिसकी उसे ज़रूरत है। कोई भी test कुछ call नहीं करता; pytest नामों के पीछे-पीछे चलता है, test_total से cart तक और cart से empty_cart तक, और उन्हें उसी क्रम में बनाता है जिसकी उस श्रृंखला को ज़रूरत है।
इसे होते हुए देखना — --setup-show
यह जानने के लिए कि कौन-से fixtures चले, आपको print calls जोड़ने की ज़रूरत नहीं है। --setup-show हर setup और teardown को प्रिंट करता है:
pytest -q --setup-show
SETUP F empty_cart
test_cart.py::test_new_cart_is_empty (fixtures used: empty_cart) .
TEARDOWN F empty_cart
SETUP F empty_cart
SETUP F cart (fixtures used: empty_cart)
test_cart.py::test_total (fixtures used: cart, empty_cart) .
TEARDOWN F cart
TEARDOWN F empty_cart
2 passed in 0.01sइसे ऊपर से पढ़ें। F का मतलब है function scope — एक test के लिए बनाया गया और उसके बाद फेंक दिया गया। empty_cart का setup दो बार होता है, हर test के लिए एक बार, और यही पिछले सेक्शन वाली ताज़गी है। जहाँ cart का उपयोग होता है, वहाँ empty_cart का setup पहले होता है, क्योंकि cart उस पर निर्भर है। और teardowns setups के उल्टे क्रम में आते हैं: जो सबसे बाद में बना, वह सबसे पहले हटाया गया।
(अगर आपने plugins इंस्टॉल किए हैं, तो उनके अपने fixtures के लिए अतिरिक्त लाइनें दिख सकती हैं। वे उनकी हैं, आपकी नहीं।)
अब तक teardown ने कुछ भी दिखाई देने वाला काम नहीं किया, क्योंकि पलटने के लिए कुछ था ही नहीं। अगले सेक्शन में यह बदलता है।
yield — पहले setup, फिर सफ़ाई
कुछ setup अपने पीछे ऐसा कुछ छोड़ जाते हैं जिसे पलटना ज़रूरी है: बंद करने के लिए कोई connection, हटाने के लिए कोई फ़ाइल, वापस रखने के लिए कोई global setting। इसके लिए fixture return की जगह yield का उपयोग करता है:
import pytest
@pytest.fixture
def connection():
print("\nconnection: open")
conn = {"orders": []}
yield conn
print("connection: close")
def test_starts_empty(connection):
print("test: running")
assert connection["orders"] == []pytest -q -s
connection: open
test: running
.connection: close
1 passed in 0.01syield से पहले का सब कुछ setup है। yield के बाद वाली वैल्यू वह है जो test को मिलती है। फिर fixture रुक जाता है — test चलता है — और जब test पूरा हो जाता है, तो pytest fixture को yield के बाद से फिर शुरू करता है, और वह बचा हुआ हिस्सा teardown है। (. "connection: close" के आगे इसलिए बैठा है क्योंकि test का परिणाम उसी पल प्रिंट हो जाता है जब test ख़त्म होता है, teardown शुरू होने से ठीक पहले।)
सफ़ाई का कोड setup वाले function में ही है, उससे कुछ लाइनें नीचे। आप एक को जोड़कर दूसरे को भूल नहीं सकते, बिना यह साफ़ दिखे।
दो yield fixtures के साथ, जिनमें से एक दूसरे पर निर्भर हो, क्रम वही होता है जिसकी ओर --setup-show ने इशारा किया था:
import pytest
@pytest.fixture
def connection():
print("\n1. connection: open")
conn = {"orders": []}
yield conn
print("5. connection: close")
@pytest.fixture
def saved_order(connection):
print("2. saved_order: insert")
connection["orders"].append("A-100")
yield "A-100"
connection["orders"].remove("A-100")
print("4. saved_order: delete")
def test_order_is_saved(connection, saved_order):
print("3. test: running")
assert saved_order in connection["orders"]pytest -q -s
1. connection: open
2. saved_order: insert
3. test: running
.4. saved_order: delete
5. connection: close
1 passed in 0.01ssetup निर्भरता की श्रृंखला में नीचे से ऊपर की ओर जाता है; teardown वापस नीचे की ओर आता है। यही एकमात्र क्रम है जो काम करता है: saved_order अपनी row को connection के ज़रिए हटाता है, इसलिए जब वह ऐसा करे तब connection का खुला रहना ज़रूरी है।
test फ़ेल होने पर भी teardown चलता है
जब test अपने अंत में खुद ही सफ़ाई कर सकता है, तो सफ़ाई के लिए fixture की मेहनत क्यों करें? आज़माकर देखें। OPEN हर उस चीज़ का प्रतीक है जिसे कोई test पीछे छोड़ सकता है — एक खुला connection, एक अस्थायी फ़ाइल, एक बदली हुई setting:
OPEN = []
def test_broken():
OPEN.append("connection")
assert OPEN == ["A-100"]
OPEN.remove("connection")
def test_nothing_left_open():
assert OPEN == []pytest -q --tb=no
FF [100%]
=========================== short test summary info ============================
FAILED test_cleanup_in_body.py::test_broken - AssertionError: assert ['connec...
FAILED test_cleanup_in_body.py::test_nothing_left_open - AssertionError: asse...
2 failed in 0.01s(--tb=no tracebacks को छिपा देता है और सिर्फ़ सारांश रखता है।) एक ग़लती, दो विफलताएँ। फ़ेल होने वाला assert एक exception उठाता है, और exception उठते ही test वहीं ख़त्म हो जाता है, इसलिए उसके नीचे की सफ़ाई वाली लाइन कभी चली ही नहीं। connection OPEN में ही रह गया, और अगला test — जो पूरी तरह सही था — विरासत में मिली गड़बड़ी के कारण फ़ेल हो गया। किसी बड़े suite में, वही दूसरी विफलता है जिस पर आप पूरी दोपहर लगा देते।
अब वही काम, लेकिन सफ़ाई yield के बाद:
import pytest
OPEN = []
@pytest.fixture
def connection():
OPEN.append("connection")
yield "connection"
OPEN.remove("connection")
def test_broken(connection):
assert OPEN == ["A-100"]
def test_nothing_left_open():
assert OPEN == []pytest -q --tb=no
F. [100%]
=========================== short test summary info ============================
FAILED test_cleanup_in_fixture.py::test_broken - AssertionError: assert ['con...
1 failed, 1 passed in 0.01sएक ग़लती, एक विफलता। टूटा हुआ test अब भी रिपोर्ट होता है — fixture कुछ नहीं छिपाता — लेकिन उसकी गड़बड़ी अगले test तक नहीं पहुँची।
यही कारण है, और इससे निकलने वाला नियम यह है: सफ़ाई का कोड कभी test की body के अंत में नहीं जाता। वह fixture के yield के बाद जाता है।
fixture या साधारण function?
setup साझा करने का एकमात्र तरीका fixture नहीं है। एक साधारण function — मान लीजिए cart_with(*lines), जो दी गई lines से एक कार्ट बनाता है — भी काम करता है, और नीचे दिया गया पूर्ण उदाहरण ऐसे ही एक function का उपयोग करता है।
फ़ैसला करने वाला अंतर यह है: test किसी helper को call करता है, और उसे arguments दे सकता है। test किसी fixture को call नहीं कर सकता; वह सिर्फ़ उसका नाम ले सकता है। fixture उसे माँगने वाले हर test के लिए हमेशा एक ही चीज़ बनाता है।
इसलिए:
- fixture का उपयोग करें जब कई tests को एक जैसी शुरुआती स्थिति चाहिए, और ख़ासकर तब जब उस स्थिति को बाद में साफ़ करना ज़रूरी हो। गारंटी वाला teardown सिर्फ़ fixture को मिलता है।
- helper function का उपयोग करें जब हर test को थोड़ा अलग ऑब्जेक्ट चाहिए — इन lines वाला कार्ट, उस नाम वाला user। arguments के लिए ही तो functions होते हैं।
ये दोनों अच्छी तरह साथ काम करते हैं: एक fixture किसी helper को call कर सकता है, जैसा अगला उदाहरण करता है। (एक ऐसा fixture बनाने का तरीका भी है जो एक function लौटाता है — बारहवें अध्याय का "factory" pattern। अभी आपको इसकी ज़रूरत नहीं है।)
पूर्ण उदाहरण
एक दुकान जहाँ discount codes एक module-level डिक्शनरी में रहते हैं। किसी code को रजिस्टर करने से global state बदलती है, इसलिए जो test किसी code को रजिस्टर करे, उसे उसे दोबारा हटाना भी होगा।
shop.py:
DISCOUNTS = {}
def register_discount(code, percent):
DISCOUNTS[code] = percent
def remove_discount(code):
del DISCOUNTS[code]
class Cart:
def __init__(self):
self.lines = []
def add(self, name, price, quantity=1):
if quantity < 1:
raise ValueError(f"quantity must be at least 1: {quantity}")
self.lines.append((name, price, quantity))
def total(self, code=None):
amount = sum(price * quantity for _, price, quantity in self.lines)
if code is not None:
amount = amount * (100 - DISCOUNTS[code]) / 100
return round(amount, 2)test_shop.py:
import pytest
from shop import Cart, register_discount, remove_discount
def cart_with(*lines):
# A plain helper: it takes arguments, so each test can ask for its own cart.
cart = Cart()
for name, price, quantity in lines:
cart.add(name, price, quantity)
return cart
@pytest.fixture
def cart():
# The cart most tests share. Built fresh for every test that asks.
return cart_with(("pen", 15.0, 3), ("bag", 850.0, 1))
@pytest.fixture
def summer_code():
# Changes global state, so it must undo that change afterwards.
register_discount("SUMMER", 10)
yield "SUMMER"
remove_discount("SUMMER")
def test_total(cart):
assert cart.total() == 895.0
def test_summer_discount(cart, summer_code):
assert cart.total(summer_code) == 805.5
def test_code_is_gone_afterwards(cart):
with pytest.raises(KeyError):
cart.total("SUMMER")
def test_small_cart(summer_code):
cart = cart_with(("pen", 15.0, 2))
assert cart.total(summer_code) == 27.0
def test_zero_quantity_is_refused():
with pytest.raises(ValueError):
Cart().add("pen", 15.0, 0)pytest -q --setup-show
SETUP F cart
test_shop.py::test_total (fixtures used: cart) .
TEARDOWN F cart
SETUP F cart
SETUP F summer_code
test_shop.py::test_summer_discount (fixtures used: cart, summer_code) .
TEARDOWN F summer_code
TEARDOWN F cart
SETUP F cart
test_shop.py::test_code_is_gone_afterwards (fixtures used: cart) .
TEARDOWN F cart
SETUP F summer_code
test_shop.py::test_small_cart (fixtures used: summer_code) .
TEARDOWN F summer_code
test_shop.py::test_zero_quantity_is_refused .
5 passed in 0.01sयहाँ तीन बातें ध्यान देने योग्य हैं।
पहला, हर test सिर्फ़ वही माँगता है जिसका वह उपयोग करता है। test_small_cart को एक अलग कार्ट चाहिए था, इसलिए उसने cart माँगा ही नहीं; उसने अपनी lines के साथ helper को call किया — और --setup-show पुष्टि करता है कि उसके लिए cart कभी बना ही नहीं। test_zero_quantity_is_refused को सिर्फ़ एक ख़ाली कार्ट और एक call चाहिए, इसलिए वह कोई fixture उपयोग नहीं करता; यह योजना वाली "ऐसी शुरुआती स्थिति जिसे कोई और साझा नहीं करता" वाली row है।
दूसरा, test_code_is_gone_afterwards असल में fixture का test है। यह test_summer_discount के बाद चलता है और जाँचता है कि SUMMER अब मौजूद नहीं है। यह क्यों मायने रखता है, यह देखने के लिए summer_code में yield और सफ़ाई वाली लाइन की जगह return "SUMMER" लिख दें:
pytest -q --tb=no
..F.. [100%]
=========================== short test summary info ============================
FAILED test_shop.py::test_code_is_gone_afterwards - Failed: DID NOT RAISE Key...
1 failed, 4 passed in 0.01scode उस test से बाहर निकलकर, जिसने उसे रजिस्टर किया था, अगले test में रिस गया। उस जाँच के बिना यह रिसाव चुपचाप हो जाता।
तीसरा, helper और fixture साथ मिलकर काम करते हैं। cart_with जानता है कि कार्ट कैसे बनाना है; cart fixture तय करता है कि ज़्यादातर tests कौन-सा कार्ट साझा करें।
जब यह काम न करे
fixture 'crat' not found
_________________________ ERROR at setup of test_empty _________________________
file .../test_typo.py, line 11
def test_empty(crat):
E fixture 'crat' not found
> available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, capteesys, cart, doctest_namespace, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, subtests, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory
> use 'pytest --fixtures [testpath]' for help on them.किसी parameter का नाम किसी fixture से मेल नहीं खाया। या तो उसकी स्पेलिंग ग़लत है — यहाँ cart की जगह crat, जो लिस्ट में ठीक वहीं मौजूद है — या function में @pytest.fixture वाली लाइन नहीं है, और उस स्थिति में वह लिस्ट में होगा ही नहीं। ध्यान दें कि यहाँ E और ERROR है, F और FAILED नहीं: test कभी शुरू ही नहीं हुआ।
Fixture "cart" called directly
Fixture "cart" called directly. Fixtures are not meant to be called directly,
but are created automatically when test functions request them as parameters.test ने अपनी body में cart() लिखा। fixture को parameter के रूप में नाम देकर माँगा जाता है, उसे कभी call नहीं किया जाता। अगर आप सच में उसे अपने arguments के साथ call करना चाहते हैं, तो आपको असल में एक helper function चाहिए।
फ़ेल होने की जगह ERROR at setup of test_total exception test चलने से पहले fixture के अंदर उठा — उदाहरण के लिए setup में किसी ग़लत add() से आया ValueError: quantity must be at least 1: 0। E का मतलब है "तैयारी टूट गई"; F का मतलब है "दावा ग़लत निकला"। पहले fixture में देखें।
fixture function has more than one 'yield' एक fixture ठीक एक बार yield करता है। setup yield के ऊपर जाता है, सफ़ाई उसके नीचे; दो वैल्यूज़ के लिए एक tuple yield करें या दो fixtures लिखें।
एक test अकेले पास होता है और बाकियों के साथ फ़ेल होता है tests के बीच कुछ साझा हो रहा है — कोई module-level लिस्ट या डिक्शनरी, या कोई global जिसे किसी fixture ने बदला और वापस नहीं रखा। उसे एक fixture में बनाएँ, और अगर वह global state है, तो yield के बाद बदलाव को पलट दें।
सफ़ाई का कोड नहीं चला सफ़ाई का कोड test की body में किसी फ़ेल होने वाले assert के बाद है, या fixture return का उपयोग करता है। उसे किसी yield fixture में, yield के बाद ले जाएँ।
चरण 4 / 6 — अनुमान
अपनी समझ की जाँच करें
दोनों टेस्ट एक ही fixture माँगते हैं, और दोनों लिस्ट में कुछ जोड़ते हैं। pytest -q की आख़िरी लाइन क्या कहेगी?
import pytest
@pytest.fixture
def basket():
return []
def test_one(basket):
basket.append("pen")
assert basket == ["pen"]
def test_two(basket):
basket.append("bag")
assert basket == ["bag"]- A1 failed, 1 passed — test_two को ['pen', 'bag'] मिलता है
- B2 failed
- C2 passed
- D1 passed, 1 error
इसे pytest -q -s से चलाया गया। pytest के अपने डॉट और सारांश को छोड़कर, तीनों लाइनें किस क्रम में प्रिंट होंगी?
import pytest
@pytest.fixture
def db():
print("open")
yield "db"
print("close")
def test_query(db):
print("query")- Aopen close query
- Bquery open close
- Copen query
- Dopen query close
यह टेस्ट fail होता है। समस्या कहाँ है?
import pytest
@pytest.fixture
def cart():
return ["pen", "bag"]
def test_count():
assert len(cart()) == 2- Afixture को एक tuple लौटाना चाहिए
- Bटेस्ट ख़ुद fixture को कॉल कर रहा है; इसके बजाय उसे `cart` को पैरामीटर के रूप में लेना चाहिए
- Cfixture में `return` नहीं, `yield` चाहिए
- Dfixture पर `len()` का इस्तेमाल नहीं हो सकता
उत्तर देने के लिए अकाउंट आवश्यक है
अपने उत्तर जाँचने के लिए साइन इन करें
प्रश्न ऊपर दिए गए हैं, और मन में उत्तर सोचना ही मुख्य कार्य है। सही उत्तर, व्याख्या और तीन-स्तरीय संकेत देखने के लिए साइन इन करें।
आपकी बारी
एक module-level LOG = [] और एक Library class के साथ library.py लिखें: add(title); borrow(title), जो title के शेल्फ़ पर न होने पर ValueError उठाए और अन्यथा उसे हटाकर LOG में "borrow <title>" जोड़े; give_back(title); और available(), जो शेल्फ़ पर मौजूद titles की sorted लिस्ट लौटाए।
फिर test_library.py लिखें। पहले योजना बनाएँ — शुरुआती स्थिति वाले कॉलम के साथ एक टेबल, जैसी इस अध्याय की शुरुआत में थी — और फिर लिखें:
- एक fixture
libraryजो तीन किताबों वाली एकLibraryलौटाए, और कम से कम तीन tests जो उसका उपयोग करें — जिनमें से एक कोई किताब borrow करे, ताकि बाद का कोई test दिखा सके कि शेल्फ़ फिर से भरा हुआ है - एक fixture
borrowedजोlibraryपर निर्भर हो, एक किताब borrow करे और उसका title लौटाए, और दो tests जो उसका उपयोग करें - एक
yieldfixtureclean_logजो test से पहले और उसके बाद फिर सेLOGको ख़ाली करे; एक test जो इसका उपयोग करे, और उसके बाद एक test जोLOG == []को assert करे - एक साधारण helper
library_with(*titles), जिसका उपयोगlibraryकरे और वह एक test भी जिसे ख़ाली शेल्फ़ चाहिए
pytest -q चलाएँ, फिर pytest -q --setup-show, और हर लाइन को अपनी अपेक्षा से मिलाकर देखें।
अंत में, इसे जानबूझकर तोड़ें: clean_log में yield और उसके बाद वाली लाइन की जगह return LOG लिखें। कौन-सा test फ़ेल होता है, और क्या वह वही test है जिसने fixture का उपयोग किया था?
समाधान
पहले योजना:
| Case | शुरुआती स्थिति | क्रिया | अपेक्षित | |---|---|---|---| | शेल्फ़ पर तीन किताबें | Dune, Emma, Ulysses | available() | तीनों, sorted | | borrow करने से किताब हटती है | वही | borrow("Dune") | Emma, Ulysses | | tests के बीच कोई रिसाव नहीं | वही | — | अब भी तीन | | दो बार borrow नहीं हो सकता | वही, Emma borrowed | borrow("Emma") | ValueError | | वापस करना | वही, Emma borrowed | give_back("Emma") | Emma शेल्फ़ पर | | borrow करना log होता है | वही, LOG ख़ाली | borrow("Ulysses") | LOG == ["borrow Ulysses"] | | log साफ़ हो गया | ऊपर वाले test के बाद | — | LOG == [] | | किनारे का case: ख़ाली शेल्फ़ | कोई किताब नहीं | available() | [] |
दो शुरुआती स्थितियाँ दोहराई जाती हैं — तीन किताबों वाला शेल्फ़ और वह शेल्फ़ जिससे Emma निकाली जा चुकी है — इसलिए वे fixtures बन गईं। ख़ाली log की ज़रूरत सिर्फ़ एक बार है, लेकिन वह global state को छूता है जिसे बाद में वापस ठीक करना ज़रूरी है, और इसकी गारंटी सिर्फ़ fixture देता है; इसलिए वह भी एक fixture है। ख़ाली शेल्फ़ की ज़रूरत एक बार है और वह पीछे कुछ नहीं छोड़ता, इसलिए वह helper का उपयोग करता है।
library.py:
LOG = []
class Library:
def __init__(self):
self.shelf = set()
def add(self, title):
self.shelf.add(title)
def borrow(self, title):
if title not in self.shelf:
raise ValueError(f"not on the shelf: {title}")
self.shelf.remove(title)
LOG.append(f"borrow {title}")
def give_back(self, title):
self.shelf.add(title)
def available(self):
return sorted(self.shelf)test_library.py:
import pytest
from library import LOG, Library
def library_with(*titles):
# Helper: each caller chooses its own shelf.
library = Library()
for title in titles:
library.add(title)
return library
@pytest.fixture
def library():
return library_with("Dune", "Emma", "Ulysses")
@pytest.fixture
def borrowed(library):
library.borrow("Emma")
return "Emma"
@pytest.fixture
def clean_log():
LOG.clear()
yield LOG
LOG.clear()
def test_three_books_on_the_shelf(library):
assert library.available() == ["Dune", "Emma", "Ulysses"]
def test_borrow_removes_the_book(library):
library.borrow("Dune")
assert library.available() == ["Emma", "Ulysses"]
def test_next_test_still_sees_three(library):
assert len(library.available()) == 3
def test_cannot_borrow_twice(library, borrowed):
with pytest.raises(ValueError):
library.borrow(borrowed)
def test_give_back_returns_it(library, borrowed):
library.give_back(borrowed)
assert borrowed in library.available()
def test_borrow_is_logged(library, clean_log):
library.borrow("Ulysses")
assert clean_log == ["borrow Ulysses"]
def test_log_is_empty_afterwards():
assert LOG == []
def test_empty_library():
library = library_with()
assert library.available() == []pytest -q
........ [100%]
8 passed in 0.01s--setup-show के साथ आप देखेंगे कि SETUP F library उसका उपयोग करने वाले छह tests में से हर एक के लिए एक बार आता है, borrowed का setup library के बाद होता है और teardown उससे पहले, और आख़िरी दो tests के लिए कोई fixture लाइन नहीं आती।
और प्रयोग, yield की जगह return LOG के साथ:
pytest -q --tb=no
......F. [100%]
=========================== short test summary info ============================
FAILED test_library.py::test_log_is_empty_afterwards - AssertionError: assert...
1 failed, 7 passed in 0.01sफ़ैसले, एक-एक करके:
libraryreturn करता है, yield नहीं। एक नईLibrarytest के बाहर कुछ नहीं बदलती, इसलिए पलटने को कुछ नहीं है।yieldउन fixtures के लिए है जिन्हें कुछ साफ़ करना हो; यह शैली का चुनाव नहीं है।test_next_test_still_sees_threeजानबूझकर रखा गया है। यह ठीक उस test के बाद चलता है जिसनेDuneborrow की थी, और पास होता है क्योंकिlibraryफिर से बनाया गया।--setup-showमें आप इसे गिन सकते हैं: हर test के लिए एक बारSETUP F library।borrowedtitle लौटाता है। tests"Emma"दोहराने की जगहborrowedका उपयोग करते हैं, इसलिए अगर fixture बदल दे कि वह कौन-सी किताब borrow करता है, तो tests भी साथ चलते हैं। और क्योंकिborrowedlibraryपर निर्भर है, दोनों को माँगने वाले दोनों tests को एक ही library मिलती है — वही जिससे Emma ली गई थी।clean_logyieldके दोनों ओर सफ़ाई करता है। पहले, क्योंकिborrow()को call करने वाले दूसरे tests —test_borrow_removes_the_book,borrowedfixture — पहले हीLOGमें लिख चुके होते हैं; test को एक जानी-पहचानी स्थिति से शुरू होना चाहिए। बाद में, ताकि अगले test तक कुछ न रिसे।- *प्रयोग दूसरे test में फ़ेल होता है।*
test_borrow_is_loggedअब भी पास होता है;test_log_is_empty_afterwards, जिसने कुछ ग़लत नहीं किया, फ़ेल होता है। छूटी हुई सफ़ाई हमेशा ऐसी ही दिखती है: विफलता कहीं और दिखाई देती है। और ठीक इसी कारण समाधान में एक ऐसा test है जो सफ़ाई की जाँच करता है। library_withएक helper है, fixture नहीं, क्योंकि जो एक test इसका उपयोग करता है उसे अलग शेल्फ़ चाहिए, और arguments सिर्फ़ function ले सकता है।libraryभी इसे call करता है, इसलिए library को कैसे भरा जाए, यह जानकारी एक ही जगह रहती है।
Step 6 of 6
चुनौती — the chapter quiz
सरल से कठिन — दस प्रश्न, अंतिम वाले जानबूझकर चुनौतीपूर्ण बनाए गए हैं।
Sign in to take the quiz