fixture का scope और conftest.py — सेटअप को सुरक्षित रूप से साझा करना
धीमा सेटअप एक बार बनाकर साझा करना: पाँच scopes, --setup-show, साझा state का ख़तरा और ScopeMismatch, बिना import के fixtures और डायरेक्टरी-वार override के लिए conftest.py, autouse की क़ीमत, और fixtures बनने का क्रम।
- 1समस्या
- 2समझें
- 3उदाहरण
- 4अनुमान
- 5स्वयं करें
- 6चुनौती
वह समस्या जिसे हम हल कर रहे हैं
पिछले अध्याय के fixtures हर टेस्ट के लिए नए सिरे से बनाए जा रहे थे। डिफ़ॉल्ट के तौर पर यही सही है — जब तक कि जो चीज़ बन रही है वह महंगी न हो। यहाँ एक डेटाबेस कनेक्शन है जिसे खुलने में आधा सेकंड लगता है, जैसे किसी असली सर्वर के साथ हैंडशेक में लग सकता है:
test_users.py:
import sqlite3
import time
import pytest
def connect():
time.sleep(0.5) # stands in for a slow server handshake
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE users (name TEXT)")
return conn
@pytest.fixture
def db():
conn = connect()
yield conn
conn.close()
def test_starts_empty(db):
count = db.execute("SELECT COUNT(*) FROM users").fetchone()[0]
assert count == 0
def test_insert_one(db):
db.execute("INSERT INTO users VALUES ('asha')")
count = db.execute("SELECT COUNT(*) FROM users").fetchone()[0]
assert count == 1
def test_names_are_text(db):
db.execute("INSERT INTO users VALUES ('ravi')")
name = db.execute("SELECT name FROM users").fetchone()[0]
assert name == "ravi"pytest -q --durations=3 बताता है कि समय कहाँ गया:
... [100%]
============================= slowest 3 durations ==============================
0.50s setup test_users.py::test_starts_empty
0.50s setup test_users.py::test_insert_one
0.50s setup test_users.py::test_names_are_text
3 passed in 1.65sतीन टेस्ट, तीन कनेक्शन, डेढ़ सेकंड सिर्फ़ हाथ मिलाने में। तीन सौ टेस्ट हों तो ढाई मिनट कुछ भी न करते हुए बीतेंगे — और इतना धीमा टेस्ट सूट धीरे-धीरे कोई चलाता ही नहीं।
यह अध्याय जिस सवाल का जवाब देता है: किसी चीज़ को एक बार बनाकर कैसे साझा करें — इस तरह कि जो टेस्ट उसे साझा करते हैं, वे एक-दूसरे के रास्ते में न आएँ?
इस अध्याय के अंत में आप कर पाएंगे
- किसी fixture का scope चुनना:
function,class,module,packageयाsession --setup-showसे fixtures को बनते और हटते हुए देखना- समझाना कि बदलने वाली state रखने वाला चौड़े scope का fixture टेस्टों को एक-दूसरे पर निर्भर क्यों बना देता है — और उसे ठीक करना
ScopeMismatchएरर पढ़कर जानना कि कौन-सा fixture बदलना हैconftest.pyके ज़रिए fixtures साझा करना, और किसी एक डायरेक्टरी के लिए fixture को override करना- बताना कि
autouse=Trueकी क़ीमत क्या है, और--fixturesसे सभी उपलब्ध fixtures की सूची देखना - पहले से बताना कि fixtures किस क्रम में बनेंगे
ज़रूरी शर्तें: fixtures।
टेस्ट लिखने से पहले
Scope कोई ऐसी चीज़ नहीं है जिसे अंत में धीमे सूट को तेज़ करने के लिए जोड़ा जाए। यह पहला टेस्ट लिखने से पहले ही तय होता है, सेटअप के हर हिस्से के बारे में दो सवालों से: इसे बनाने में कितना खर्च आता है, और क्या कोई टेस्ट इसे बदलता है? पहले इनके जवाब दीजिए, सही scope अपने आप निकल आएगा।
कॉन्ट्रैक्ट। यहाँ जिस कोड का टेस्ट हो रहा है वह एक छोटी users टेबल है। उसका वादा: नए डेटाबेस में कोई यूज़र नहीं होता; एक यूज़र डालने पर ठीक एक row जुड़ती है; जो नाम आप डालते हैं वही नाम वापस मिलता है। इनमें से हर वादा चाहे पहले कोई भी टेस्ट चला हो तब भी टिकना चाहिए। यह आख़िरी शर्त ही असल में इस अध्याय का विषय है।
जो पहले से तैयार होना चाहिए। pytest इंस्टॉल किया हुआ एक virtual environment; sqlite3, जो पायथन के साथ ही आता है, इसलिए कुछ इंस्टॉल नहीं करना; टेस्ट फ़ाइलों से टेस्ट होने वाला कोड import हो सके (यहाँ फ़ाइलें एक ही डायरेक्टरी में साथ-साथ हैं, और उसी डायरेक्टरी से चलाई जाती हैं); और यह फ़ैसला कि साझा fixtures कहाँ रहेंगे — टेस्ट ट्री के सबसे ऊपर एक conftest.py।
टेस्ट प्लान। तीन केस, जिनमें से हर एक को अकेले और किसी भी क्रम में पास होना चाहिए:
| केस | इनपुट | अपेक्षित | |---|---|---| | नया डेटाबेस | कुछ नहीं डाला गया | COUNT(*) है 0 | | एक insert | 'asha' | COUNT(*) है 1 | | नाम का वापस आना | 'ravi' | पढ़ा गया पहला नाम 'ravi' है |
सेटअप प्लान। फिर उसी तरह की टेबल उस चीज़ के लिए जिस पर टेस्ट खड़े हैं:
| रिसोर्स | बनाने का खर्च | क्या कोई टेस्ट इसे बदलता है? | scope | |---|---|---|---| | कनेक्शन + टेबल | धीमा (आधा सेकंड) | नहीं | चौड़ा — module या session | | टेबल की rows | मुफ़्त | हाँ, हर insert पर | function — हर टेस्ट के बाद रीसेट |
आख़िरी कॉलम ध्यान से पढ़िए: एक ही ऑब्जेक्ट, कनेक्शन, एक धीमी चीज़ और एक बदलने वाली चीज़ — दोनों को रखता है। इसीलिए अध्याय के अंत में एक की जगह दो fixtures बनते हैं — एक चौड़ा, एक संकरा।
क्या टेस्ट न करें। sqlite3 को ख़ुद नहीं; आपका कोड कभी जितना टेस्ट होगा, उससे कहीं ज़्यादा गहराई से वह टेस्ट किया हुआ है। fixtures को सीधे नहीं — fixture की जाँच उन टेस्टों से होती है जो उसे इस्तेमाल करते हैं, और --setup-show पढ़कर। और आधे सेकंड की देरी को भी नहीं: गति --durations से मापने की चीज़ है, assert करने की नहीं।
scope= — किसके प्रति एक बार?
किसी fixture का scope बताता है कि एक वैल्यू कितनी देर ज़िंदा रहेगी, इससे पहले कि pytest उसे फेंककर दूसरी बनाए। डिफ़ॉल्ट है function: हर टेस्ट के लिए एक। एक लाइन बदलिए:
@pytest.fixture(scope="module")
def db():
conn = connect()
yield conn
conn.close()pytest -q:
..F [100%]
=================================== FAILURES ===================================
_____________________________ test_names_are_text ______________________________
db = <sqlite3.Connection object at 0x79350aa45c60>
def test_names_are_text(db):
db.execute("INSERT INTO users VALUES ('ravi')")
name = db.execute("SELECT name FROM users").fetchone()[0]
> assert name == "ravi"
E AssertionError: assert 'asha' == 'ravi'
E
E - ravi
E + asha
test_users.py:35: AssertionError
=========================== short test summary info ============================
FAILED test_users.py::test_names_are_text - AssertionError: assert 'asha' == ...
1 failed, 2 passed in 0.52sदो चीज़ें हुईं। रन 1.65 सेकंड से घटकर 0.52 पर आ गया — तीन की जगह एक कनेक्शन। और जो टेस्ट अभी-अभी पास हो रहा था, वह अब फ़ेल हो रहा है। इस फ़ेलियर को याद रखिए; इसका अपना अलग हिस्सा नीचे है। पहले scopes ख़ुद।
ये पाँच हैं, सबसे संकरे से सबसे चौड़े तक:
| scope | किसके प्रति एक वैल्यू | कब हटाया जाता है | |---|---|---| | function | टेस्ट (डिफ़ॉल्ट) | हर टेस्ट के बाद | | class | टेस्ट क्लास | क्लास के आख़िरी टेस्ट के बाद | | module | टेस्ट फ़ाइल | फ़ाइल के आख़िरी टेस्ट के बाद | | package | वह डायरेक्टरी जहाँ fixture परिभाषित है | उस डायरेक्टरी के आख़िरी टेस्ट के बाद, सब-डायरेक्टरी समेत | | session | पूरा pytest रन | रन के बिल्कुल आख़िरी टेस्ट के बाद |
एक काउंटर अंतर को दिखा देता है। नीचे का हर fixture गिनता है कि उसे कितनी बार बनाया गया:
test_scopes.py:
from collections import Counter
import pytest
made = Counter()
@pytest.fixture(scope="session")
def per_session():
made["session"] += 1
@pytest.fixture(scope="module")
def per_module():
made["module"] += 1
@pytest.fixture(scope="class")
def per_class():
made["class"] += 1
@pytest.fixture
def per_test():
made["function"] += 1
class TestFirst:
def test_a(self, per_session, per_module, per_class, per_test):
pass
def test_b(self, per_session, per_module, per_class, per_test):
pass
class TestSecond:
def test_c(self, per_session, per_module, per_class, per_test):
pass
def test_report():
print(dict(made))pytest -q -s (-s से print का आउटपुट बाहर आ पाता है):
...{'session': 1, 'module': 1, 'class': 2, 'function': 3}
.
4 passed in 0.01sतीन टेस्टों ने fixtures इस्तेमाल किए। function fixture तीन बार बना, class fixture दो बार (दो क्लासें), और module व session fixture एक-एक बार।
होते हुए देखना: --setup-show
गिनना काम करता है, लेकिन pytest पूरी टाइमलाइन ख़ुद बनाकर दिखा सकता है। उसी फ़ाइल को pytest -q --setup-show के साथ चलाइए:
SETUP S per_session
SETUP M per_module
SETUP C per_class
SETUP F per_test
test_scopes.py::TestFirst::test_a (fixtures used: per_class, per_module, per_session, per_test) .
TEARDOWN F per_test
SETUP F per_test
test_scopes.py::TestFirst::test_b (fixtures used: per_class, per_module, per_session, per_test) .
TEARDOWN F per_test
TEARDOWN C per_class
SETUP C per_class
SETUP F per_test
test_scopes.py::TestSecond::test_c (fixtures used: per_class, per_module, per_session, per_test) .
TEARDOWN F per_test
TEARDOWN C per_class
test_scopes.py::test_report .
TEARDOWN M per_module
TEARDOWN S per_session
4 passed in 0.01sSETUP के बाद वाला अक्षर scope है — Session, Package, Module, Class, Function — और indentation उन्हें एक-दूसरे के अंदर सजाता है। ऊपर से नीचे पढ़िए और साफ़ दिखेगा कि कौन-सी वैल्यू कब पैदा हुई और कब ख़त्म हुई। जब भी शक हो कि कोई fixture क्या कर रहा है, सबसे पहले यही flag उठाइए।
साझा fixture अपनी state भी साझा करता है
फ़ेलियर पर लौटते हैं। scope="module" के साथ तीनों टेस्टों को वही एक कनेक्शन मिला। test_insert_one ने 'asha' को जोड़ा और किसी ने उसे हटाया नहीं। फिर test_names_are_text ने 'ravi' जोड़ा, पहला नाम माँगा, और मिला 'asha'।
इस बग का सबसे साफ़ संकेत है ऐसा टेस्ट जो अकेले चलाने पर पास हो जाए। pytest -q test_users.py::test_names_are_text:
. [100%]
1 passed in 0.50sअकेले पास; साथ में फ़ेल। जो टेस्ट इस पर निर्भर हों कि उनसे पहले क्या चला, वे धीमे टेस्टों से भी बुरे हैं: एक क्रम में फ़ेल होते हैं, दूसरे में पास, और हर बार एक दोपहर बर्बाद करते हैं।
नियम: महंगी चीज़ का scope चौड़ा करें, state का scope संकरा रखें। fixture को दो में बाँटिए — एक चौड़ा, जिसके पास कनेक्शन है; और एक function-scope वाला, जो हर टेस्ट के बाद सफ़ाई करता है:
@pytest.fixture(scope="module")
def connection():
conn = connect()
yield conn
conn.close()
@pytest.fixture
def db(connection):
yield connection
connection.rollback() # undo whatever this test wroteटेस्ट नहीं बदलते — वे अब भी db माँगते हैं। pytest -q --durations=3:
... [100%]
============================= slowest 3 durations ==============================
0.50s setup test_users.py::test_starts_empty
(2 durations < 0.005s hidden. Use -vv to show these durations.)
3 passed in 0.51sएक कनेक्शन, तीन पास होते टेस्ट। पहले INSERT पर sqlite3 एक transaction खोलता है, और rollback() पूरा transaction फेंक देता है, इसलिए हर टेस्ट ख़ाली टेबल से शुरू होता है।
सफ़ाई fixture में क्यों रखें, बजाय इसके कि हर टेस्ट DELETE FROM users से शुरू हो? क्योंकि जिस सफ़ाई को हर टेस्ट को याद रखना पड़े, उसे कोई एक टेस्ट भूलेगा ही — और जो भूलता है, फ़ेल वह कभी नहीं होता; फ़ेल होता है अगला टेस्ट। fixture में, सफ़ाई db माँगने वाले हर टेस्ट के बाद होती है, उन टेस्टों के बाद भी जो अगले साल कोई ऐसा व्यक्ति लिखेगा जिसने यह फ़ाइल कभी नहीं पढ़ी। और DELETE की जगह rollback() क्यों? rollback टेस्ट का लिखा सब कुछ, हर टेबल में, पलट देता है — fixture को यह जानने की ज़रूरत नहीं कि वे बदलाव क्या थे।
--setup-show यह बँटवारा दिखाता है:
SETUP M connection
SETUP F db (fixtures used: connection)
test_users.py::test_starts_empty (fixtures used: connection, db) .
TEARDOWN F db
SETUP F db (fixtures used: connection)
test_users.py::test_insert_one (fixtures used: connection, db) .
TEARDOWN F db
SETUP F db (fixtures used: connection)
test_users.py::test_names_are_text (fixtures used: connection, db) .
TEARDOWN F db
TEARDOWN M connection
3 passed in 0.51sfunction-scope वाला fixture बेझिझक किसी चौड़े fixture को माँग सकता है, जैसे यहाँ db माँग रहा है connection को। उल्टी दिशा की अनुमति नहीं है।
ScopeMismatch — चौड़ा संकरे को नहीं माँग सकता
test_mismatch.py:
import sqlite3
import pytest
@pytest.fixture
def db_name():
return ":memory:"
@pytest.fixture(scope="session")
def connection(db_name):
conn = sqlite3.connect(db_name)
yield conn
conn.close()
def test_connects(connection):
assert connection.execute("SELECT 1").fetchone() == (1,)pytest -q:
E [100%]
==================================== ERRORS ====================================
_______________________ ERROR at setup of test_connects ________________________
ScopeMismatch: You tried to access the function scoped fixture db_name with a session scoped request object. Requesting fixture stack:
test_mismatch.py:11: def connection(db_name)
Requested fixture:
test_mismatch.py:6: def db_name()
=========================== short test summary info ============================
ERROR test_mismatch.py::test_connects - Failed: ScopeMismatch: You tried to a...
1 error in 0.01sसोचिए इस माँग का मतलब क्या होता। connection पूरे रन तक जीता है; db_name पहले टेस्ट के बाद ही फेंक दिया जाता है। session वाली वैल्यू ऐसी चीज़ को पकड़े रहती जो उसके अपने ही नियमों से अब मौजूद नहीं। इसलिए pytest मना कर देता है — और ध्यान दीजिए, यह setup के समय की Error है, Failure नहीं: टेस्ट चला ही नहीं।
संदेश दोनों fixtures और दोनों लाइनों के नाम बताता है। हल हमेशा एक ही है: माँगे गए fixture को माँगने वाले के कम से कम बराबर चौड़ा कीजिए — यहाँ db_name पर @pytest.fixture(scope="session") — या माँगने वाले को संकरा कीजिए।
conftest.py — बिना import के fixtures
जैसे ही किसी दूसरी टेस्ट फ़ाइल को db चाहिए, fixture को कॉपी करना ग़लत जवाब है। उसे ठीक conftest.py नाम की फ़ाइल में ले जाइए। pytest उस फ़ाइल को ख़ुद लोड करता है, और उसी डायरेक्टरी और उसके नीचे का हर टेस्ट उसके fixtures माँग सकता है — बिना किसी import के।
एक सब-डायरेक्टरी वाला प्रोजेक्ट:
conftest.py
test_users.py
reports/
conftest.py
test_reports.pyconftest.py:
import sqlite3
import time
import pytest
def connect():
time.sleep(0.5) # stands in for a slow server handshake
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE users (name TEXT, city TEXT)")
return conn
@pytest.fixture(scope="session")
def connection():
"""One database connection, shared by the whole run."""
conn = connect()
yield conn
conn.close()
@pytest.fixture
def db(connection):
"""The shared connection; every write is rolled back after the test."""
yield connection
connection.rollback()test_users.py — एक भी import नहीं:
def test_starts_empty(db):
count = db.execute("SELECT COUNT(*) FROM users").fetchone()[0]
assert count == 0
def test_insert_one(db):
db.execute("INSERT INTO users VALUES ('asha', 'Dhaka')")
count = db.execute("SELECT COUNT(*) FROM users").fetchone()[0]
assert count == 1scope अब session है: प्रोजेक्ट की हर फ़ाइल के लिए एक ही कनेक्शन।
एक डायरेक्टरी के लिए fixture को override करना
reports/ के सभी टेस्टों को ऐसा डेटाबेस चाहिए जिसमें पहले से यूज़र हों। उस डायरेक्टरी का एक conftest.py उसी नाम से fixture परिभाषित कर सकता है; reports/ के टेस्टों के लिए नज़दीकी परिभाषा जीतती है। और अगर वह अपना ही नाम माँगे, तो उसे parent का संस्करण मिलता है:
reports/conftest.py:
import pytest
@pytest.fixture
def db(db):
"""The parent db, with three users already in it."""
db.executemany(
"INSERT INTO users VALUES (?, ?)",
[("asha", "Dhaka"), ("ravi", "Pune"), ("omar", "Cairo")],
)
return dbreports/test_reports.py:
def test_user_count(db):
count = db.execute("SELECT COUNT(*) FROM users").fetchone()[0]
assert count == 3
def test_cities_sorted(db):
rows = db.execute("SELECT city FROM users ORDER BY city").fetchall()
assert [city for (city,) in rows] == ["Cairo", "Dhaka", "Pune"]pytest -v:
============================= test session starts ==============================
collecting ... collected 4 items
reports/test_reports.py::test_user_count PASSED [ 25%]
reports/test_reports.py::test_cities_sorted PASSED [ 50%]
test_users.py::test_starts_empty PASSED [ 75%]
test_users.py::test_insert_one PASSED [100%]
============================== 4 passed in 0.51s ===============================क्रम देखिए। reports/ के टेस्ट पहले चले और हर बार तीन यूज़र डाले — फिर भी उनके बाद चले test_starts_empty को टेबल ख़ाली मिली। parent db ने पहले से डाली गई rows को भी rollback कर दिया, क्योंकि override उसके ऊपर बना है। --setup-show दोनों परतें दिखाता है, दोनों का नाम db:
SETUP S connection
SETUP F db (fixtures used: connection)
SETUP F db (fixtures used: db)
reports/test_reports.py::test_user_count (fixtures used: connection, db) .
TEARDOWN F db
TEARDOWN F db(यह पहला टेस्ट है; बाक़ी इसी पैटर्न को दोहराते हैं।) override सिर्फ़ नीचे की ओर पहुँचता है: ऊपर की डायरेक्टरी के टेस्ट उसे कभी नहीं देखते।
--fixtures — मैं यहाँ क्या-क्या माँग सकता हूँ?
जब fixtures कई conftest.py फ़ाइलों में बिखरे हों, तो यह देखने का तरीका चाहिए कि कोई ख़ास टेस्ट क्या माँग सकता है। pytest --fixtures reports उस डायरेक्टरी से दिखने वाली हर चीज़ की सूची देता है। सूची pytest के बिल्ट-इन fixtures (अध्याय नौ) और इंस्टॉल किए गए plugins के fixtures से शुरू होती है; आपके वाले अंत में आते हैं:
------------------------ fixtures defined from conftest ------------------------
connection [session scope] -- conftest.py:15
One database connection, shared by the whole run.
db -- conftest.py:23
The shared connection; every write is rolled back after the test.
db -- reports/conftest.py:5
The parent db, with three users already in it.हर एंट्री में नाम, scope (अगर वह function नहीं है), फ़ाइल और लाइन, और docstring की पहली लाइनें होती हैं। यही आख़िरी हिस्सा कारण है कि हर साझा fixture पर एक लाइन की docstring लिखी जाए: लोग असल में यही डॉक्यूमेंटेशन पढ़ते हैं।
autouse=True — और उसकी क़ीमत
autouse=True वाला fixture अपनी पहुँच के हर टेस्ट में इस्तेमाल होता है, टेस्ट उसे माँगे या न माँगे। ऊपर के conftest.py में जोड़ा गया यह fixture जाँचता है कि कोई टेस्ट rows पीछे तो नहीं छोड़ रहा:
@pytest.fixture(autouse=True)
def no_rows_left(connection):
"""After every test, check that the users table is empty again."""
yield
count = connection.execute("SELECT COUNT(*) FROM users").fetchone()[0]
assert count == 0, f"{count} row(s) left behind"यह काम करता है — जो टेस्ट सीधे connection से लिखता है और rollback छोड़ देता है, वह teardown पर पकड़ा जाता है। लेकिन अब एक ऐसा टेस्ट जोड़िए जिसका डेटाबेस से कोई लेना-देना नहीं:
test_text.py:
def slugify(title):
return title.strip().lower().replace(" ", "-")
def test_slugify():
assert slugify(" Hello World ") == "hello-world"pytest -q --setup-show test_text.py:
SETUP S connection
SETUP F no_rows_left (fixtures used: connection)
test_text.py::test_slugify (fixtures used: connection, no_rows_left) .
TEARDOWN F no_rows_left
TEARDOWN S connection
1 passed in 0.50sएक लाइन का string टेस्ट अब डेटाबेस खोलता है और आधा सेकंड लेता है। यही autouse की क़ीमत है: यह अदृश्य है — test_slugify में कहीं नहीं लिखा कि डेटाबेस शामिल है — और यह बिना शर्त है — अपने conftest.py के नीचे के हर टेस्ट में चलता है, उनमें भी जिन्हें इसकी ज़रूरत नहीं। autouse को सिर्फ़ सस्ती और सच में सार्वभौमिक चीज़ों के लिए रखिए, और उसे सबसे संकरे उस conftest.py में रखिए जो ज़रूरतमंद टेस्टों को ढकता हो।
कौन-सा fixture पहले बनता है?
जब किसी टेस्ट को कई fixtures चाहिए, pytest उन्हें तीन नियमों से क्रम देता है:
- चौड़ा scope पहले। session, फिर module, फिर function — टेस्ट के signature में क्रम चाहे जो हो।
- एक ही scope के भीतर,
autousefixtures पहले। - निर्भरताएँ उन fixtures से पहले जिन्हें उनकी ज़रूरत है।
test_order.py:
import pytest
log = []
@pytest.fixture(scope="session")
def server():
log.append("server")
@pytest.fixture(scope="module")
def connection(server):
log.append("connection")
@pytest.fixture
def user():
log.append("user")
@pytest.fixture
def cart(user):
log.append("cart")
@pytest.fixture(autouse=True)
def audit():
log.append("audit")
def test_order(cart, connection):
print(log)pytest -q -s:
['server', 'connection', 'audit', 'user', 'cart']
.
1 passed in 0.01sटेस्ट ने connection से पहले cart माँगा था, लेकिन connection — module scope — पहले बना, अपनी निर्भरता server के बाद। function-scope वाले fixtures में autouse audit पहले आया, फिर user, जिसकी cart को ज़रूरत है। teardown ठीक उल्टे क्रम में चलता है। इन नियमों के अलावा क्रम पर भरोसा मत कीजिए: अगर एक fixture का दूसरे से पहले होना ज़रूरी है, तो उसे parameter के रूप में नाम देकर निर्भरता बना दीजिए।
एक पूरा उदाहरण
एक छोटा shop module, जिसका टेस्ट एक साझा in-memory डेटाबेस पर हो रहा है।
shop.py:
def add_order(conn, customer, total):
conn.execute(
"INSERT INTO orders (customer, total) VALUES (?, ?)", (customer, total)
)
def revenue(conn):
return conn.execute("SELECT COALESCE(SUM(total), 0) FROM orders").fetchone()[0]
def top_customer(conn):
row = conn.execute(
"SELECT customer FROM orders GROUP BY customer "
"ORDER BY SUM(total) DESC LIMIT 1"
).fetchone()
return row[0] if row else Noneconftest.py:
import sqlite3
import time
import pytest
@pytest.fixture(scope="session")
def connection():
"""One in-memory database with the orders table, for the whole run."""
time.sleep(0.5) # stands in for a slow server handshake
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE orders (customer TEXT, total REAL)")
yield conn
conn.close()
@pytest.fixture
def db(connection):
"""The shared connection; whatever a test writes is rolled back."""
yield connection
connection.rollback()test_orders.py:
from shop import add_order, revenue, top_customer
def test_revenue_starts_at_zero(db):
assert revenue(db) == 0
def test_add_order_counts_toward_revenue(db):
add_order(db, "asha", 250.0)
assert revenue(db) == 250.0
def test_no_top_customer_without_orders(db):
assert top_customer(db) is Nonereports/conftest.py:
import pytest
from shop import add_order
@pytest.fixture
def db(db):
"""The parent db, with four orders from three customers already in it."""
add_order(db, "asha", 250.0)
add_order(db, "ravi", 900.0)
add_order(db, "omar", 120.0)
add_order(db, "asha", 700.0)
return dbreports/test_reports.py:
from shop import revenue, top_customer
def test_total_revenue(db):
assert revenue(db) == 1970.0
def test_top_customer_by_total(db):
assert top_customer(db) == "asha"pytest -v --durations=1:
============================= test session starts ==============================
collecting ... collected 5 items
reports/test_reports.py::test_total_revenue PASSED [ 20%]
reports/test_reports.py::test_top_customer_by_total PASSED [ 40%]
test_orders.py::test_revenue_starts_at_zero PASSED [ 60%]
test_orders.py::test_add_order_counts_toward_revenue PASSED [ 80%]
test_orders.py::test_no_top_customer_without_orders PASSED [100%]
============================= slowest 1 durations ==============================
0.50s setup reports/test_reports.py::test_total_revenue
============================== 5 passed in 0.51s ===============================तीन बातें ध्यान देने लायक हैं।
पहली, धीमा सेटअप एक बार हुआ, रन के पहले टेस्ट पर, और कहीं नहीं। यही session scope का फ़ायदा है।
दूसरी, test_orders.py के टेस्ट तब चले जब report टेस्ट कुल आठ ऑर्डर डाल चुके थे, फिर भी उन्हें revenue शून्य मिला। connection.rollback() की जगह pass लिखकर फिर से चलाइए, साझा state की क़ीमत तुरंत दिख जाती है:
FAILED test_orders.py::test_revenue_starts_at_zero - assert 3940.0 == 0
FAILED test_orders.py::test_add_order_counts_toward_revenue - assert 4190.0 =...
FAILED test_orders.py::test_no_top_customer_without_orders - AssertionError: ...
3 failed, 2 passed in 0.52sतीसरी, कोई भी टेस्ट फ़ाइल fixture import नहीं करती। test_orders.py सिर्फ़ shop import करती है, यानी टेस्ट होने वाला कोड; fixtures नाम से सबसे नज़दीकी conftest.py से आते हैं।
जब कुछ टूट जाए
ScopeMismatch: You tried to access the function scoped fixture db_name with a session scoped request object. एक चौड़े fixture ने संकरे fixture को माँगा। माँगे गए fixture (यहाँ db_name) को माँगने वाले के कम से कम बराबर scope तक चौड़ा कीजिए, या माँगने वाले को संकरा कीजिए। Requesting fixture stack के नीचे की दो लाइनें बताती हैं कि कौन क्या है।
AssertionError: assert 'asha' == 'ravi' — लेकिन अकेले चलाने पर टेस्ट पास हो जाता है function से चौड़े किसी fixture के ज़रिए टेस्टों के बीच state रिस रही है। कौन-सा fixture साझा है यह देखने के लिए --setup-show से चलाइए, फिर उसे बाँटिए: महंगे ऑब्जेक्ट को चौड़ा रखिए और एक function-scope वाला fixture जोड़िए जो हर टेस्ट के बदलाव पलट दे।
fixture 'report_title' not found fixture मौजूद है, लेकिन वहाँ नहीं जहाँ से यह टेस्ट उसे देख सके। एक conftest.py सिर्फ़ अपनी डायरेक्टरी और उसके नीचे की डायरेक्टरियों की सेवा करता है — reports/conftest.py का fixture ऊपर की डायरेक्टरी के टेस्ट के लिए अदृश्य है। उसे ऐसे conftest.py में ऊपर ले जाइए जो दोनों को ढके। pytest --fixtures path/to/test_file.py ठीक-ठीक दिखाता है कि वह फ़ाइल क्या-क्या देख सकती है।
db के rollback करने के बावजूद एक टेस्ट की rows दूसरे में दिख रही हैं किसी ने commit() बुलाया है। rollback सिर्फ़ वही फेंकता है जो commit नहीं हुआ, इसलिए जो कोड commit करता है — या जो टेस्ट db की जगह सीधे connection इस्तेमाल करता है — वह स्थायी रूप से लिखता है। no_rows_left जैसा पहरेदार fixture इसे साफ़ दिखने वाले ERROR at teardown ... AssertionError: 1 row(s) left behind में बदल देता है।
डेटाबेस को न छूने वाला एक टेस्ट धीमा हो गया है ऊपर कहीं एक autouse fixture महंगे वाले पर निर्भर है। उस टेस्ट पर pytest --setup-show चलाने से वे सभी fixtures दिखते हैं जो उसने असल में इस्तेमाल किए, वे भी जो उसने कभी माँगे नहीं।
टेस्ट फ़ाइल में from conftest import db conftest.py से import मत कीजिए। pytest उसके fixtures आपको नाम से पहले ही दे देता है, और import conftest को वह conftest.py मिलता है जो पायथन को पहले मिल जाए। ऊपर के प्रोजेक्ट में, सबसे ऊपर की test_users.py में यह लाइन जोड़ने पर reports/ वाला import हुआ — पहले से डेटा भरने वाला override — और test_starts_empty assert 3 == 0 के साथ फ़ेल हुआ। import किया गया fixture टेस्ट फ़ाइल में ही परिभाषित माना जाता है, जो हर conftest.py पर भारी पड़ता है। fixtures सिर्फ़ parameter के रूप में नाम देकर माँगिए, और कुछ नहीं।
चरण 4 / 6 — अनुमान
अपनी समझ की जाँच करें
pytest -q -s से चलाने पर test_report क्या प्रिंट करेगा?
import pytest
calls = {"module": 0, "function": 0}
@pytest.fixture(scope="module")
def conn():
calls["module"] += 1
@pytest.fixture
def row(conn):
calls["function"] += 1
def test_a(row):
pass
def test_b(row):
pass
def test_c(conn):
pass
def test_report():
print(calls)- A{'module': 3, 'function': 3}
- B{'module': 1, 'function': 3}
- C{'module': 1, 'function': 2}
- D{'module': 3, 'function': 2}
यह टेस्ट चलने से पहले ही ScopeMismatch देता है। सबसे छोटा सही हल कौन-सा है?
import pytest
@pytest.fixture
def settings():
return {"db": ":memory:"}
@pytest.fixture(scope="session")
def connection(settings):
return settings["db"]
def test_connects(connection):
assert connection == ":memory:"- A`test_connects` को एक क्लास के अंदर रखना
- B`settings` को `@pytest.fixture(scope="session")` देना
- C`connection` में `autouse=True` जोड़ना
- Dफ़ाइल में `settings` को `connection` के नीचे ले जाना
दोनों conftest.py में db है। reports/test_reports.py का test_count कौन-सा db पाता है?
# conftest.py
@pytest.fixture
def db(connection):
yield connection
connection.rollback()
# reports/conftest.py
@pytest.fixture
def db(db):
db.execute("INSERT INTO users VALUES ('asha', 'Dhaka')")
return db
# reports/test_reports.py
def test_count(db):
...- Aऊपर वाले `conftest.py` का, क्योंकि वह पहले लोड होता है
- Bकोई नहीं — एक ही नाम के दो fixtures होना एरर है
- C`reports/` वाला, लेकिन उसका `db` parameter ख़ुद को ही पाता है, इसलिए अनंत recursion होता है
- D`reports/` वाला, जो ऊपर वाले को पाता है और उसके ऊपर एक row जोड़ता है
उत्तर देने के लिए अकाउंट आवश्यक है
अपने उत्तर जाँचने के लिए साइन इन करें
प्रश्न ऊपर दिए गए हैं, और मन में उत्तर सोचना ही मुख्य कार्य है। सही उत्तर, व्याख्या और तीन-स्तरीय संकेत देखने के लिए साइन इन करें।
आपकी बारी
एक library.py के इर्द-गिर्द एक छोटा प्रोजेक्ट बनाइए, जिसमें दो फ़ंक्शन हों: add_book(conn, title, author) और books_by(conn, author), जो शीर्षकों की क्रमबद्ध सूची लौटाता है।
- सबसे ऊपर के
conftest.pyमें session-scope वाला एकconnectionfixture लिखिए जोsqlite3.connect(":memory:")खोले, एकbooks (title TEXT, author TEXT)टेबल बनाए, और धीमे सर्वर की जगह आधा सेकंड सोए। इसे एक docstring दीजिए। - इसके ऊपर function-scope वाला एक
dbfixture जोड़िए जो हर टेस्ट के बाद rollback करे। - तीन टेस्टों वाली
test_library.pyलिखिए, जिनमें से हर एक किताबें जोड़करbooks_byजाँचे।pytest -q --durations=3चलाकर पक्का कीजिए कि आधा सेकंड एक ही बार दिखता है। search/conftest.pyबनाइए जोdbको override करके पहले से पाँच किताबों के साथ शुरू कराए, औरsearch/test_search.pyजिसमें उन पर निर्भर दो टेस्ट हों।pytest --setup-showचलाकरsearch/के टेस्टों के आउटपुट मेंdbकी दोनों परतें ढूँढिए। फिरpytest --fixtures searchचलाकर देखिए कि दोनों docstrings दिखती हैं।
फिर इसे जान-बूझकर दो बार तोड़िए। पहली बार, rollback() वाली लाइन की जगह pass लिखकर पूरा सूट चलाइए — कौन-से टेस्ट फ़ेल होते हैं, और क्या हर एक अकेले चलाने पर पास होता है? दूसरी बार, connection को निर्भर होने के लिए function-scope वाला एक db_name fixture दीजिए, और ScopeMismatch संदेश को लाइन-दर-लाइन पढ़िए।
यही पाँच मिनट इस अध्याय का सार हैं। scope गति का फ़ैसला है, लेकिन इससे पैदा होने वाले बग isolation के बग हैं — और उन्हें पहचानना सीखने का एक ही तरीका है, कुछ को ख़ुद पैदा करना।
समाधान
ढाँचा:
library.py
conftest.py
test_library.py
search/
conftest.py
test_search.pylibrary.py:
def add_book(conn, title, author):
conn.execute("INSERT INTO books VALUES (?, ?)", (title, author))
def books_by(conn, author):
rows = conn.execute(
"SELECT title FROM books WHERE author = ? ORDER BY title", (author,)
).fetchall()
return [title for (title,) in rows]conftest.py:
import sqlite3
import time
import pytest
@pytest.fixture(scope="session")
def connection():
"""One in-memory database with the books table, for the whole run."""
time.sleep(0.5) # stands in for a slow server handshake
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE books (title TEXT, author TEXT)")
yield conn
conn.close()
@pytest.fixture
def db(connection):
"""The shared connection; every test's writes are rolled back."""
yield connection
connection.rollback()test_library.py:
from library import add_book, books_by
def test_unknown_author_has_no_books(db):
assert books_by(db, "Nobody") == []
def test_one_book(db):
add_book(db, "Gitanjali", "Tagore")
assert books_by(db, "Tagore") == ["Gitanjali"]
def test_titles_come_back_sorted(db):
add_book(db, "Kabuliwala", "Tagore")
add_book(db, "Gitanjali", "Tagore")
assert books_by(db, "Tagore") == ["Gitanjali", "Kabuliwala"]search/conftest.py:
import pytest
from library import add_book
@pytest.fixture
def db(db):
"""The parent db, with five books by three authors already in it."""
add_book(db, "Gitanjali", "Tagore")
add_book(db, "Kabuliwala", "Tagore")
add_book(db, "Godaan", "Premchand")
add_book(db, "Nirmala", "Premchand")
add_book(db, "Palace Walk", "Mahfouz")
return dbsearch/test_search.py:
from library import books_by
def test_two_books_by_premchand(db):
assert books_by(db, "Premchand") == ["Godaan", "Nirmala"]
def test_author_match_is_exact(db):
assert books_by(db, "tagore") == []pytest -q --durations=3:
..... [100%]
============================= slowest 3 durations ==============================
0.50s setup search/test_search.py::test_two_books_by_premchand
(2 durations < 0.005s hidden. Use -vv to show these durations.)
5 passed in 0.51spytest -q --setup-show search दोनों टेस्टों में से हर एक के लिए वही तस्वीर दिखाता है जो अध्याय में थी: SETUP F db (fixtures used: connection) और उसके बाद SETUP F db (fixtures used: db) — parent परत, फिर उसके ऊपर override। pytest --fixtures search connection को [session scope] के साथ, दोनों db fixtures को उनकी फ़ाइल और लाइन के साथ, और तीनों docstrings दिखाता है।
पहला प्रयोग — rollback() की जगह pass — यह देता है:
FAILED test_library.py::test_one_book - AssertionError: assert ['Gitanjali',....
FAILED test_library.py::test_titles_come_back_sorted - AssertionError: assert...
2 failed, 3 passed in 0.52sऔर pytest -q test_library.py::test_one_book अकेले:
. [100%]
1 passed in 0.50ssearch/ के टेस्ट पहले चले और पाँच किताबें पीछे छोड़ गए, इसलिए test_one_book को एक की जगह Tagore के दो शीर्षक मिले। अकेले वह पास होता है। यही रिसती state की पहचान है, बिल्कुल अध्याय की तरह। दूसरे प्रयोग में पाँचों टेस्ट E बन जाते हैं, हर एक में वही ScopeMismatch जो आप पहले देख चुके हैं, इस बार conftest.py की ओर इशारा करते हुए: def connection(db_name) माँगने वाला है, और def db_name() वह fixture जो बहुत संकरा है।
हर फ़ैसले का कारण:
connectionsession-scope वाला है, क्योंकि यही अकेली धीमी चीज़ है, और कोई टेस्ट कनेक्शन को ख़ुद नहीं बदलता — सिर्फ़ उसके अंदर की rows बदलती हैं।dbfunction-scope वाला है, क्योंकि rows बदलती हैं। उसका पूरा काम हर टेस्ट के बाद rollback है; टेस्टों को उसके नाम के अलावा उसके बारे में कुछ जानने की ज़रूरत नहीं।- टेस्ट
dbमाँगते हैं, कभीconnectionनहीं। जो टेस्ट सीधेconnectionलेता, वह rollback छोड़ देता और उसकी rows अगले टेस्ट में रिस जातीं। search/का overridedbमाँगता है,connectionनहीं, ताकि पहले से डाली गई किताबें टेस्ट के अपने काम का हिस्सा बन जाएँ और parent का rollback उन्हें बाक़ी सब के साथ हटा दे। अगरdbकी सफ़ाई कभी बदले, तो override को छुए बिना उसे वह बदलाव मिल जाता है।test_author_match_is_exactजान-बूझकर प्लान में है: यह पक्का करता है कि"tagore"और"Tagore"एक नहीं हैं — एक ऐसी सीमा जिसे कोई एक दिन बदलना चाहेगा, और उसे जान-बूझकर बदलना चाहिए, एक फ़ेल होते टेस्ट के बताने के बाद।- हर साझा fixture पर docstring है, क्योंकि अगला व्यक्ति
pytest --fixturesसे ही जानेगा कि वह क्या-क्या माँग सकता है।
Step 6 of 6
चुनौती — the chapter quiz
सरल से कठिन — दस प्रश्न, अंतिम वाले जानबूझकर चुनौतीपूर्ण बनाए गए हैं।
Sign in to take the quiz