built-in fixtures — tmp_path, capsys और caplog
टेस्टिंग की झंझट वाली जगहों के लिए pytest के साथ आने वाले fixtures: फ़ाइलें, प्रिंट आउटपुट और लॉग। असली paths को छुए बिना CSV और JSON लिखने वाले कोड को टेस्ट करना, और यह जाँचना कि वह क्या प्रिंट और लॉग करता है।
- 1समस्या
- 2समझें
- 3उदाहरण
- 4अनुमान
- 5स्वयं करें
- 6चुनौती
वह समस्या जिसे हम हल कर रहे हैं
यहाँ एक फ़ंक्शन है जो एक CSV फ़ाइल लिखता है, और उसके लिए एक टेस्ट:
import csv
def write_csv(path, rows):
with open(path, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["product", "quantity"])
writer.writerows(rows)
def test_write_csv():
write_csv("report.csv", [("pen", 12), ("bag", 2)])
with open("report.csv") as f:
assert f.read().splitlines()[1] == "pen,12"$ pytest -q
. [100%]
1 passed in 0.08s
$ ls
report.csv test_report.pyटेस्ट पास हो गया, और पीछे कुछ छोड़ गया। report.csv अब आपके प्रोजेक्ट में, आपके कोड के ठीक बगल में पड़ी है। किसी दूसरे फ़ोल्डर से pytest चलाइए तो फ़ाइल वहाँ जा पहुँचेगी, क्योंकि "report.csv" relative है — उस जगह के सापेक्ष जहाँ आप उस समय खड़े थे। अगर दो टेस्ट दोनों report.csv लिखते हैं, तो वे एक-दूसरे का छोड़ा हुआ माल पढ़ेंगे। और अगर आपके प्रोजेक्ट में पहले से कोई असली report.csv थी, तो टेस्ट ने उसे अभी-अभी ओवरराइट कर दिया।
एक टेस्ट को दुनिया ठीक वैसी ही छोड़नी चाहिए जैसी उसे मिली थी। फ़ाइलें सबसे साफ़ उदाहरण हैं, लेकिन प्रिंट किया गया आउटपुट और लॉग मैसेज यही सवाल दूसरी तरफ़ से उठाते हैं: फ़ंक्शन कुछ बाहर भेजता है, और टेस्ट को उसे पकड़ना होता है।
पिछले दो अध्यायों में आपने fixtures लिखना सीखा। यह अध्याय उन fixtures के बारे में है जो pytest इन्हीं झंझट वाली जगहों के लिए पहले से साथ देता है — आपको बस नाम लेकर माँगना है।
इस अध्याय के अंत में आप कर पाएंगे
tmp_pathसे हर टेस्ट को उसका अपना खाली फ़ोल्डर देना, और CSV या JSON लिखने वाले फ़ंक्शन टेस्ट करनाtmp_path_factoryसे पूरे सेशन में एक अस्थायी फ़ोल्डर साझा करनाcapsysसे टेस्ट करना कि प्रोग्राम क्या प्रिंट करता है, और जानना कि कब उसकी जगहcapfdचाहिएcaplogसे टेस्ट करना कि प्रोग्राम क्या लॉग करता है,WARNINGसे नीचे के मैसेज समेतrequest.node.nameसे पता करना कि कौन-सा टेस्ट चल रहा है- इन fixtures के गलत इस्तेमाल पर आने वाले एरर पढ़ना
ज़रूरी शर्तें: fixture का scope और conftest.py।
टेस्ट लिखने से पहले
जो कोड फ़ाइलों, टर्मिनल या लॉग को छूता है, उसके side effects होते हैं: उसका असली नतीजा वह है जो वह दुनिया में छोड़ जाता है, सिर्फ़ वह नहीं जो वह लौटाता है। इसलिए टेस्ट कोड लिखने से पहले लिख लीजिए कि हर असर कहाँ जाता है।
1. हर आउटपुट का नाम लिखिए। स्टॉक की पंक्तियाँ एक्सपोर्ट करने वाले फ़ंक्शन के चार तक आउटपुट हो सकते हैं:
- लौटाई गई वैल्यू (वह path जिस पर उसने लिखा)
- फ़ाइल के अंदर का कंटेंट
- वह क्या प्रिंट करता है, standard output और standard error में बँटा हुआ
- वह क्या लॉग करता है, और किस लेवल पर
हर एक अलग वादा है, और हर एक को देखने का कोई तरीका चाहिए। लौटाई गई वैल्यू आपको मुफ़्त में मिलती है। बाकी तीन के लिए pytest के पास एक-एक fixture है: फ़ाइलों के लिए tmp_path, प्रिंट हुए टेक्स्ट के लिए capsys, लॉग के लिए caplog।
2. पक्का कीजिए कि कोड टेस्ट को यह तय करने देता है कि फ़ाइलें कहाँ जाएँ। यह सवाल सबसे पहले पूछना है, क्योंकि जवाब "नहीं" हो तो कोई fixture आपको नहीं बचा सकता। जो फ़ंक्शन अपना path खुद बनाता है — open("/var/reports/stock.csv", "w") — उसे टेस्ट करने का एक ही तरीका है उस असली जगह पर लिखना। जो फ़ंक्शन फ़ोल्डर या path को parameter के रूप में लेता है, उसे tmp_path की ओर मोड़ा जा सकता है। कोड आपका है, तो उसे path लेने वाला बना दीजिए। (बदल न सकें, तो अगले अध्याय का monkeypatch निकलने का रास्ता है।)
3. अपना सेटअप जाँचिए। कुछ इंस्टॉल नहीं करना — इस अध्याय का हर fixture pytest के साथ आता है। चाहिए वही जानी-पहचानी चीज़ें: pytest वाला एक virtual environment, और टेस्ट फ़ाइल से import हो सकने वाला कोड। अपने logger का नाम जान लीजिए (logging.getLogger("export")), क्योंकि caplog.set_level उसे निशाना बना सकता है।
4. केस प्लान कीजिए। export_stock(rows, out_dir) के लिए, जो वैध पंक्तियाँ stock.csv में लिखता है, negative मात्रा पर चेतावनी देता है और एक सारांश प्रिंट करता है:
| केस | इनपुट | अपेक्षित | |---|---|---| | सामान्य रास्ता | [("pen", 12), ("eraser", 30)], tmp_path | फ़ाइल में हेडर + 2 पंक्तियाँ; प्रिंट करता है wrote 2 rows to stock.csv | | अवैध पंक्ति | मात्रा -1 वाली एक पंक्ति | पंक्ति बाहर; उसका नाम लेती एक WARNING | | फ़ोल्डर मौजूद नहीं | tmp_path / "exports" / "2026" | फ़ोल्डर बनते हैं; फ़ाइल अंदर | | कुछ भी वैध नहीं | [("bag", -1)] | सिर्फ़ हेडर; standard error पर nothing exported | | साफ़ आउटपुट | कोई भी वैध पंक्तियाँ | standard error पर कुछ नहीं |
5. तय कीजिए कि क्या टेस्ट नहीं करना। यह टेस्ट मत कीजिए कि पायथन का csv मॉड्यूल कॉमा को सही quote करता है, या json.dumps वैध JSON बनाता है — वे किसी और के वादे हैं, पहले से टेस्ट किए हुए। पूरा caplog.text हूबहू assert मत कीजिए; उसमें फ़ाइल नाम और लाइन नंबर होते हैं जो कोड एडिट करते ही बदल जाते हैं। और tmp_path की सटीक लोकेशन टेस्ट मत कीजिए; वह हर मशीन पर अलग है। अपने फ़ैसले टेस्ट कीजिए: कौन-सी पंक्तियाँ रहती हैं, क्या कहाँ जाता है, मैसेज क्या कहते हैं।
बाकी अध्याय उस टेबल के हर कॉलम के लिए औज़ार देता है, और पूर्ण उदाहरण उस प्लान को लागू करता है।
tmp_path — हर टेस्ट के लिए एक नया फ़ोल्डर
tmp_path माँगिए और pytest आपके टेस्ट को एक ऐसी डायरेक्टरी देता है जो एक पल पहले मौजूद नहीं थी:
def test_first(tmp_path):
print(tmp_path)
print(type(tmp_path))
def test_second(tmp_path):
print(tmp_path)$ pytest -q -s
/tmp/pytest-of-you/pytest-0/test_first0
<class 'pathlib.PosixPath'>
./tmp/pytest-of-you/pytest-0/test_second0
.
2 passed in 0.07s(-s pytest की अपनी आउटपुट कैप्चरिंग बंद कर देता है ताकि print टर्मिनल तक पहुँचे। Windows पर path आपके यूज़र के Temp फ़ोल्डर के अंदर कहीं से शुरू होता है और टाइप WindowsPath होता है।)
उस आउटपुट में तीन बातें पढ़ने लायक हैं:
- यह एक
pathlib.Pathहै, string नहीं। जोड़िए/से, पढ़िए.read_text()से, और जाँचिए.exists()से। - हर टेस्ट को अपना फ़ोल्डर मिलता है, टेस्ट के नाम पर।
test_first0औरtest_second0कभी नहीं मिलते, इसलिए दोनोंreport.csvनाम की फ़ाइल लिख सकते हैं। - हर रन को एक नया बेस फ़ोल्डर मिलता है:
pytest-0, फिरpytest-1, और आगे। pytest पिछले तीन रन के फ़ोल्डर रखता है ताकि फ़ेल होने के बाद आप अंदर देख सकें, और पुराने खुद मिटा देता है। आपको कभी सफ़ाई नहीं करनी पड़ती।
फ़ाइल लिखने वाले फ़ंक्शन को टेस्ट करना
अब CSV टेस्ट दोबारा, इस बार सही तरीके से — साथ में JSON फ़ंक्शन की एक जोड़ी, क्योंकि सेटिंग्स सेव करना लगभग हर प्रोग्राम का दूसरा फ़ाइल-लेखन काम है:
import csv
import json
def write_csv(path, rows):
with open(path, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["product", "quantity"])
writer.writerows(rows)
def save_settings(path, settings):
path.write_text(json.dumps(settings, indent=2))
def load_settings(path):
return json.loads(path.read_text())
def test_write_csv(tmp_path):
target = tmp_path / "report.csv"
write_csv(target, [("pen", 12), ("bag", 2)])
assert target.exists()
assert target.read_text().splitlines() == [
"product,quantity",
"pen,12",
"bag,2",
]
def test_csv_reads_back(tmp_path):
target = tmp_path / "report.csv"
write_csv(target, [("pen", 12)])
with target.open(newline="") as f:
rows = list(csv.DictReader(f))
assert rows == [{"product": "pen", "quantity": "12"}]
def test_settings_round_trip(tmp_path):
target = tmp_path / "settings.json"
settings = {"currency": "BDT", "tax": 0.15}
save_settings(target, settings)
assert load_settings(target) == settings
assert '"currency": "BDT"' in target.read_text()$ pytest -q
... [100%]
3 passed in 0.07s
$ ls
test_files.pyइस बार पीछे कुछ नहीं छूटा। पैटर्न हमेशा एक ही है: tmp_path के अंदर एक path बनाइए, उसे टेस्ट हो रहे कोड को दीजिए, फिर फ़ाइल वापस पढ़कर जाँचिए।
जाँचने के दो तरीके हैं, और ऊपर के टेस्ट दोनों इस्तेमाल करते हैं। test_write_csv हूबहू टेक्स्ट जाँचता है — सबसे सख़्त टेस्ट, और तब सही जब कोई दूसरा प्रोग्राम फ़ाइल पढ़ेगा। test_csv_reads_back और test_settings_round_trip फ़ाइल को वैसे पढ़ते हैं जैसे कोई असली पाठक पढ़ेगा और डेटा की तुलना करते हैं। ध्यान दीजिए कि DictReader ने "12" लौटाया, एक string: CSV में संख्याएँ नहीं होतीं, सिर्फ़ टेक्स्ट। इस तरह का सरप्राइज़ round-trip टेस्ट में ही सामने आता है।
tmp_path खाली शुरू होता है। अगर आपका कोड मानकर चलता है कि कोई सबफ़ोल्डर मौजूद है, तो या तो टेस्ट में उसे (tmp_path / "exports").mkdir() से बनाइए या — बेहतर — कोड से ही बनवाइए। कोई न बनाए तो क्या होता है, यह "जब यह काम न करे" में दिखेगा।
tmp_path_factory — पूरे सेशन के लिए एक फ़ोल्डर
tmp_path function-scoped है: हर टेस्ट के लिए नया फ़ोल्डर। कभी-कभी यह बर्बादी है। मान लीजिए कई टेस्ट एक बड़ी सैंपल फ़ाइल पढ़ते हैं जिसे बनाने में समय लगता है। पिछले अध्याय के हिसाब से आप scope="session" की ओर हाथ बढ़ाते — लेकिन session fixture tmp_path इस्तेमाल नहीं कर सकता, क्योंकि पूरे सेशन तक जीने वाला fixture किसी ऐसी चीज़ पर निर्भर नहीं हो सकता जो हर टेस्ट के बाद फेंक दी जाती है।
इसके लिए tmp_path_factory है, जो खुद session-scoped है। आप उससे mktemp के ज़रिए फ़ोल्डर माँगते हैं:
import pytest
@pytest.fixture(scope="session")
def big_catalogue(tmp_path_factory):
folder = tmp_path_factory.mktemp("data")
target = folder / "catalogue.csv"
lines = ["product,quantity"]
lines += [f"item{i},{i}" for i in range(10_000)]
target.write_text("\n".join(lines))
print("built", target)
return target
def test_has_header(big_catalogue):
assert big_catalogue.read_text().startswith("product,quantity")
def test_has_every_row(big_catalogue):
assert len(big_catalogue.read_text().splitlines()) == 10_001$ pytest -q -s
built /tmp/pytest-of-you/pytest-6/data0/catalogue.csv
..
2 passed in 0.07sदो टेस्ट के लिए built एक ही बार आया। mktemp("data") नाम में एक संख्या जोड़ता है — data0, और दोबारा बुलाएँ तो data1 — इसलिए दो कॉल कभी टकराते नहीं।
कीमत वही है जो पिछले अध्याय में थी: दोनों टेस्ट अब एक फ़ाइल साझा करते हैं। यहाँ यह ठीक है क्योंकि वे सिर्फ़ पढ़ते हैं। जो टेस्ट साझा फ़ाइल को बदलता है, उसे tmp_path में अपनी कॉपी चाहिए।
capsys — प्रोग्राम क्या प्रिंट करता है, यह टेस्ट करना
उस गलती से शुरू करते हैं जो लगभग हर कोई एक बार करता है। एक फ़ंक्शन अभिवादन प्रिंट करता है; टेस्ट उसका नतीजा जाँचता है:
def greet(name):
print(f"hello, {name}")
def test_greet():
assert greet("Rina") == "hello, Rina"$ pytest -q
F [100%]
=================================== FAILURES ===================================
__________________________________ test_greet __________________________________
def test_greet():
> assert greet("Rina") == "hello, Rina"
E AssertionError: assert None == 'hello, Rina'
E + where None = greet('Rina')
test_wrong.py:6: AssertionError
----------------------------- Captured stdout call -----------------------------
hello, Rina
=========================== short test summary info ============================
FAILED test_wrong.py::test_greet - AssertionError: assert None == 'hello, Rina'
1 failed in 0.09sprint टेक्स्ट भेजता है; लौटाता नहीं, इसलिए greet लौटाता है None। लेकिन रिपोर्ट के नीचे देखिए: Captured stdout call — pytest ने टेक्स्ट पहले ही पकड़ लिया है। टर्मिनल साफ़ रखने के लिए वह हर टेस्ट में ऐसा करता है। capsys उसे हाथ में लेने का तरीका है:
def greet(name):
print(f"hello, {name}")
def test_greet(capsys):
greet("Rina")
assert capsys.readouterr().out == "hello, Rina\n"$ pytest -q
. [100%]
1 passed in 0.07sपहले फ़ंक्शन बुलाइए, फिर पढ़िए उसने क्या प्रिंट किया। असली कमांड-लाइन प्रोग्राम एक से ज़्यादा लाइनें, दो streams में प्रिंट करते हैं, और एक exit code लौटाते हैं। यह रहा एक, टेस्ट के साथ:
import sys
def main(argv):
if not argv:
print("usage: stock ITEM QUANTITY", file=sys.stderr)
return 2
item, quantity = argv[0], int(argv[1])
print(f"{item}: {quantity} in stock")
if quantity < 5:
print("low stock")
return 0
def test_prints_the_stock(capsys):
code = main(["pen", "12"])
captured = capsys.readouterr()
assert code == 0
assert captured.out == "pen: 12 in stock\n"
assert captured.err == ""
def test_warns_when_low(capsys):
main(["bag", "2"])
out = capsys.readouterr().out
assert out.splitlines() == ["bag: 2 in stock", "low stock"]
def test_usage_goes_to_stderr(capsys):
code = main([])
captured = capsys.readouterr()
assert code == 2
assert captured.out == ""
assert "usage:" in captured.err$ pytest -q
... [100%]
3 passed in 0.08scapsys.readouterr() standard output और standard error पर लिखा सब कुछ लौटाता है पिछली बार माँगने के बाद से, दो string फ़ील्ड वाले एक ऑब्जेक्ट के रूप में: .out और .err। यह एक named tuple है, इसलिए out, err = capsys.readouterr() भी चलता है।
व्यवहार में दो बारीकियाँ मायने रखती हैं:
printएक newline जोड़ता है। कैप्चर हुआ टेक्स्ट"pen: 12 in stock\n"है,"pen: 12 in stock"नहीं। कई लाइनों के लिए.splitlines()तुलना के लिए एक साफ़ लिस्ट देता है।- एरर
errमें जाते हैं। usage मैसेज की जगह standard error है, औरtest_usage_goes_to_stderrजाँचता है कि वह वहीं गया और standard output पर कुछ नहीं गया।
"पिछली बार माँगने के बाद से" शब्दशः है — पढ़ने से बफ़र खाली हो जाता है:
def test_read_twice(capsys):
print("first")
one = capsys.readouterr()
print("second")
two = capsys.readouterr()
assert one.out == "first\n"
assert two.out == "second\n"$ pytest -q
. [100%]
1 passed in 0.07sयह काम का है: आप आउटपुट कदम-दर-कदम जाँच सकते हैं। यही कारण भी है कि दूसरा readouterr() कभी-कभी अचानक खाली लौट आता है।
capfd — जब आउटपुट पायथन से होकर नहीं जाता
capsys पायथन के sys.stdout और sys.stderr को बदल देता है। जो कुछ सीधे ऑपरेटिंग सिस्टम के आउटपुट पर लिखता है — एक child process, एक C extension — वह इसके बगल से निकल जाता है। capfd एक स्तर नीचे, file descriptor पर पकड़ता है, और उसे भी पकड़ लेता है:
import os
def test_with_capsys(capsys):
os.system("echo from the shell")
assert capsys.readouterr().out == ""
def test_with_capfd(capfd):
os.system("echo from the shell")
assert capfd.readouterr().out == "from the shell\n"$ pytest -q
.. [100%]
2 passed in 0.07sवही readouterr(), वही .out और .err। डिफ़ॉल्ट रूप से capsys इस्तेमाल कीजिए और capfd पर तभी जाइए जब पायथन के बाहर का आउटपुट गायब हो।
caplog — प्रोग्राम क्या लॉग करता है, यह टेस्ट करना
असली प्रोग्राम प्रिंट करने के बजाय लॉग करते हैं। लॉग में एक चेतावनी भी किसी और व्यवहार जैसी ही है, इसलिए वह भी एक टेस्ट की हक़दार है:
import logging
logger = logging.getLogger("importer")
def parse_quantities(lines):
result = {}
for line in lines:
item, _, raw = line.partition(",")
if not raw.strip().isdigit():
logger.warning("skipped %r: bad quantity", item)
continue
result[item] = int(raw)
logger.info("parsed %d of %d lines", len(result), len(lines))
return result
def test_bad_row_is_logged(caplog):
result = parse_quantities(["pen,12", "bag,two"])
assert result == {"pen": 12}
assert len(caplog.records) == 1
record = caplog.records[0]
assert record.levelname == "WARNING"
assert record.getMessage() == "skipped 'bag': bad quantity"
def test_text_is_one_string(caplog):
parse_quantities(["bag,two"])
print(repr(caplog.text))
assert "bad quantity" in caplog.text$ pytest -q -s
."WARNING importer:test_importer.py:11 skipped 'bag': bad quantity\n"
.
2 passed in 0.07scaplog आपको वही लॉग तीन तरह से देता है:
caplog.records—logging.LogRecordऑब्जेक्ट्स की लिस्ट, हर मैसेज का एक। हर एक के पास है.levelname("WARNING"),.name(logger,"importer"), और.getMessage(), जो%rकी जगह उसकी वैल्यू भर देता है।caplog.messages— सिर्फ़ तैयार मैसेज strings, एक लिस्ट के रूप में।caplog.text— सब कुछ एक फ़ॉर्मैट की हुई string के रूप में। जल्दी सेinजाँच के लिए अच्छा; किसी भी सटीक चीज़ के लिए बहुत ढीला।
जब लेवल मायने रखता हो तो records चुनिए, और जब सिर्फ़ शब्द मायने रखते हों तो messages।
WARNING से नीचे के मैसेज डिफ़ॉल्ट रूप से कैप्चर नहीं होते
parse_quantities एक INFO सारांश भी लॉग करता है। उसके लिए एक टेस्ट जोड़िए:
def test_summary_is_logged(caplog):
parse_quantities(["pen,12"])
assert caplog.messages == ["parsed 1 of 1 lines"]$ pytest -q -k summary
F [100%]
=================================== FAILURES ===================================
____________________________ test_summary_is_logged ____________________________
caplog = <_pytest.logging.LogCaptureFixture object at 0x70495b4f43e0>
def test_summary_is_logged(caplog):
parse_quantities(["pen,12"])
> assert caplog.messages == ["parsed 1 of 1 lines"]
E AssertionError: assert [] == ['parsed 1 of 1 lines']
E
E Right contains one more item: 'parsed 1 of 1 lines'
E Use -v to get more diff
test_importer.py:38: AssertionError
=========================== short test summary info ============================
FAILED test_importer.py::test_summary_is_logged - AssertionError: assert [] =...
1 failed, 2 deselected in 0.08sकुछ भी कैप्चर नहीं हुआ। फ़ंक्शन ने लॉग किया था — लेकिन पायथन का logging WARNING से शुरू होता है, इसलिए INFO मैसेज caplog तक पहुँचने से पहले ही छोड़ दिया गया। टेस्ट के लिए लेवल caplog.set_level से नीचे कीजिए:
import logging
def test_summary_is_logged(caplog):
caplog.set_level(logging.INFO)
parse_quantities(["pen,12"])
assert caplog.messages == ["parsed 1 of 1 lines"]
def test_only_the_importer_logger(caplog):
caplog.set_level(logging.INFO, logger="importer")
parse_quantities(["pen,12", "bag,two"])
assert [r.levelname for r in caplog.records] == ["WARNING", "INFO"]$ pytest -q
.. [100%]
2 passed in 0.07sset_level को लॉग करने वाले कोड से पहले बुलाइए। दूसरा रूप, logger="importer" के साथ, सिर्फ़ उस एक logger का लेवल नीचे करता है, ताकि बातूनी लाइब्रेरी चुप रहें। दोनों हालात में, टेस्ट ख़त्म होने पर pytest पुराना लेवल वापस रख देता है।
request और pytestconfig
एक और built-in fixture दूसरे fixtures के अंदर दिखता है। request उस टेस्ट का ब्योरा देता है जिसने fixture माँगा; request.node.name उस टेस्ट का नाम है। फ़ाइलों को पढ़ने लायक, अनोखे नाम देने के लिए यह काम आता है:
import pytest
@pytest.fixture
def export_file(tmp_path, request):
return tmp_path / f"{request.node.name}.csv"
def test_pens(export_file):
print(export_file.name)
@pytest.mark.parametrize("quantity", [1, 50])
def test_bulk(export_file, quantity):
print(export_file.name)
def test_config(pytestconfig):
print(pytestconfig.rootpath.name, pytestconfig.getoption("verbose"))$ pytest -q -s
test_pens.csv
.test_bulk[1].csv
.test_bulk[50].csv
.shop -1
.
4 passed in 0.07sparametrize किए गए टेस्ट के नाम में उसके parameters वर्ग कोष्ठक में होते हैं। और आख़िरी टेस्ट pytestconfig दिखाता है: रन का कॉन्फ़िगरेशन ऑब्जेक्ट, जिसमें प्रोजेक्ट का root फ़ोल्डर और हर कमांड-लाइन ऑप्शन होता है (-q verbosity को -1 कर देता है)। कॉन्फ़िगरेशन और plugins वाले अध्यायों में इससे फिर मुलाकात होगी।
पूर्ण उदाहरण
export.py वैध स्टॉक पंक्तियाँ एक CSV में लिखता है, हर छोड़ी गई पंक्ति के लिए एक चेतावनी लॉग करता है, और एक सारांश प्रिंट करता है:
import csv
import logging
import sys
logger = logging.getLogger("export")
def export_stock(rows, out_dir):
"""Write the valid rows to out_dir/stock.csv and return its path."""
out_dir.mkdir(parents=True, exist_ok=True)
target = out_dir / "stock.csv"
written = 0
with target.open("w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["product", "quantity"])
for product, quantity in rows:
if quantity < 0:
logger.warning("skipped %s: negative quantity %d", product, quantity)
continue
writer.writerow([product, quantity])
written += 1
print(f"wrote {written} rows to {target.name}")
if written == 0:
print("nothing exported", file=sys.stderr)
return targettest_export.py उसके तीनों आउटपुट जाँचता है — फ़ाइल, लॉग और टर्मिनल:
import csv
from export import export_stock
ROWS = [("pen", 12), ("bag", -1), ("eraser", 30)]
def read_rows(path):
with path.open(newline="") as f:
return list(csv.reader(f))
def test_writes_only_valid_rows(tmp_path):
target = export_stock(ROWS, tmp_path)
assert target == tmp_path / "stock.csv"
assert read_rows(target) == [
["product", "quantity"],
["pen", "12"],
["eraser", "30"],
]
def test_creates_missing_folders(tmp_path):
out_dir = tmp_path / "exports" / "2026"
target = export_stock(ROWS, out_dir)
assert target.parent == out_dir
assert target.exists()
def test_skipped_row_is_logged(tmp_path, caplog):
export_stock(ROWS, tmp_path)
assert [r.levelname for r in caplog.records] == ["WARNING"]
assert caplog.messages == ["skipped bag: negative quantity -1"]
def test_prints_a_summary(tmp_path, capsys):
export_stock(ROWS, tmp_path)
captured = capsys.readouterr()
assert captured.out == "wrote 2 rows to stock.csv\n"
assert captured.err == ""
def test_empty_export_warns_on_stderr(tmp_path, capsys):
export_stock([("bag", -1)], tmp_path)
out, err = capsys.readouterr()
assert out == "wrote 0 rows to stock.csv\n"
assert err == "nothing exported\n"$ pytest -v
collecting ... collected 5 items
test_export.py::test_writes_only_valid_rows PASSED [ 20%]
test_export.py::test_creates_missing_folders PASSED [ 40%]
test_export.py::test_skipped_row_is_logged PASSED [ 60%]
test_export.py::test_prints_a_summary PASSED [ 80%]
test_export.py::test_empty_export_warns_on_stderr PASSED [100%]
============================== 5 passed in 0.09s ===============================
$ ls
export.py test_export.pyतीन बातें ध्यान देने लायक हैं।
पहली, export_stock फ़ोल्डर को एक parameter के रूप में लेता है। यही एक फ़ैसला उसे टेस्ट करने लायक बनाता है: प्रोग्राम असली फ़ोल्डर देता है, टेस्ट tmp_path देते हैं। जो फ़ंक्शन अपने अंदर "/var/reports/stock.csv" हार्ड-कोड करता है, उसे सिर्फ़ उस असली path पर लिखकर ही टेस्ट किया जा सकता है — और ठीक यही एक टेस्ट को कभी नहीं करना चाहिए।
दूसरी, हर टेस्ट सिर्फ़ वही fixtures माँगता है जिन्हें वह जाँचता है। उनमें से दो tmp_path को capsys या caplog के साथ मिलाते हैं; built-in fixtures आपस में और आपके अपने fixtures के साथ खुलकर मिलते हैं।
तीसरी, फ़ाइलें लिखने वाले पाँच टेस्ट के बाद भी working directory जस की तस है।
जब यह काम न करे
fixture 'tmp_dir' not found parameter के नाम में टाइपो। pytest fixtures को सटीक नाम से मिलाता है, और एरर आगे available fixtures: के साथ उसे पता हर fixture छापता है — tmp_path, tmp_path_factory, capsys, capfd, caplog और बाकी। सही नाम उसी लिस्ट से कॉपी कीजिए। (वहाँ tmpdir भी दिखेगा; यह पुराना संस्करण है जो pathlib.Path की जगह एक py.path ऑब्जेक्ट लौटाता है। नए कोड में tmp_path इस्तेमाल कीजिए।)
ScopeMismatch: You tried to access the function scoped fixture tmp_path with a session scoped request object. scope="session" (या "module") वाले fixture ने tmp_path माँगा। लंबे समय तक जीने वाला fixture हर-टेस्ट वाले fixture पर निर्भर नहीं हो सकता। इसकी जगह tmp_path_factory.mktemp("name") इस्तेमाल कीजिए।
FileNotFoundError: [Errno 2] No such file or directory: '/tmp/pytest-of-you/pytest-3/test_nested0/exports/report.csv' tmp_path मौजूद है, लेकिन उसके अंदर exports फ़ोल्डर नहीं — आपके लिए कोई फ़ोल्डर नहीं बनाता। पहले parent पर .mkdir(parents=True, exist_ok=True) बुलाइए, बेहतर हो कि टेस्ट हो रहे कोड के अंदर ही, जैसे export_stock करता है।
/tmp/pytest-of-you/pytest-5/test_str0report.csv पर एक फ़ाइल दिखती है कोई एरर नहीं — path strings से बनाया गया था: str(tmp_path) + "report.csv"। separator छूट गया, इसलिए फ़ाइल टेस्ट के फ़ोल्डर के अंदर नहीं, उसके बगल में पहुँची। Path पर ही / से जोड़िए: tmp_path / "report.csv"।
caplog के साथ AssertionError: assert [] == ['parsed 1 of 1 lines'] मैसेज WARNING से नीचे लॉग हुआ और कभी कैप्चर नहीं हुआ। कोड चलने से पहले caplog.set_level(logging.INFO) — या logging.DEBUG — बुलाइए।
टेक्स्ट की उम्मीद थी, पर capsys.readouterr().out है "" या तो उसी टेस्ट में किसी पहले के readouterr() ने आउटपुट पहले ही ले लिया, या वह पायथन के sys.stdout से होकर नहीं गया — एक subprocess या os.system कॉल। दूसरे मामले में capfd पर जाइए।
cannot use capfd and capsys at the same time एक टेस्ट ने दोनों माँगे। वे एक ही streams को अलग-अलग स्तर पर पकड़ते हैं; एक चुनिए।
चरण 4 / 6 — अनुमान
अपनी समझ की जाँच करें
आख़िरी लाइन के बाद captured.out में ठीक क्या है?
def test_twice(capsys):
print("first")
capsys.readouterr()
print("second")
print("third")
captured = capsys.readouterr()- A'first\nsecond\nthird\n'
- B'third\n'
- C'second\nthird\n'
- D''
restock लॉग करता है, फिर भी टेस्ट assert [] == ['restocked pen'] के साथ फ़ेल होता है। क्यों?
import logging
logger = logging.getLogger("shop")
def restock(item):
logger.info("restocked %s", item)
def test_restock_is_logged(caplog):
restock("pen")
assert caplog.messages == ["restocked pen"]- Alogger का नाम `"shop"` है; `caplog` सिर्फ़ root logger को सुनता है
- B`INFO` डिफ़ॉल्ट `WARNING` लेवल से नीचे है, इसलिए कुछ कैप्चर नहीं हुआ; पहले `caplog.set_level(logging.INFO)` बुलाइए
- C`caplog.messages` में लेवल का नाम भी होता है, इसलिए `'INFO restocked pen'` से तुलना कीजिए
- D`%s` कभी भरा नहीं जाता, इसलिए मैसेज `'restocked %s'` है
एक scope="session" fixture एक बड़ी सैंपल फ़ाइल एक बार बनाना चाहता है। फ़ाइल कहाँ जानी चाहिए?
- A`tmp_path` में — बस fixture के parameters में उसे माँग लीजिए
- B`tmpdir` में — यह `tmp_path` का session संस्करण है
- Cप्रोजेक्ट के अंदर एक `data/` फ़ोल्डर में, ताकि अगले रन तक बना रहे
- D`tmp_path_factory.mktemp("data")` जो फ़ोल्डर लौटाता है, उसमें
उत्तर देने के लिए अकाउंट आवश्यक है
अपने उत्तर जाँचने के लिए साइन इन करें
प्रश्न ऊपर दिए गए हैं, और मन में उत्तर सोचना ही मुख्य कार्य है। सही उत्तर, व्याख्या और तीन-स्तरीय संकेत देखने के लिए साइन इन करें।
आपकी बारी
notes.py नाम का एक मॉड्यूल लिखिए जिसमें export_notes(notes, out_dir) फ़ंक्शन हो। यह {"title": "Buy pens", "done": False} जैसी dictionaries की एक लिस्ट लेता है और:
out_dirमौजूद न हो तो बनाता है, और notes कोout_dir / "notes.json"में लिखता है- जिस note का title खाली है, उसके लिए एक
WARNINGलॉग करता है, और उसे फ़ाइल से बाहर रखता है - एक
INFOमैसेज लॉग करता है"exported N notes" - ख़त्म होने पर
saved notes.jsonप्रिंट करता है
फिर test_notes.py लिखिए जो, सिर्फ़ built-in fixtures से, जाँचे:
- JSON फ़ाइल वापस पढ़ने पर ठीक वही notes मिलें जो वैध थे
tmp_path / "a" / "b"जैसा गायब सबफ़ोल्डर बन जाता है- चेतावनी
levelname == "WARNING"के साथ लॉग होती है, औरINFOमैसेज भी कैप्चर होता है (उसके लिए पहले क्या बुलाना होगा?) - ठीक
saved notes.json\nप्रिंट होता है, और standard error पर कुछ नहीं जाता
pytest -q चलाइए, फिर ls (Windows पर dir)। अगर आपके प्रोजेक्ट फ़ोल्डर में कोई notes.json दिखे, तो आपका कोई टेस्ट — या आपका फ़ंक्शन — किसी असली path पर लिख रहा है। उसे ढूँढकर ठीक करना ही वह आदत है जिसके बारे में यह अध्याय है।
समाधान
पहले प्लान, "टेस्ट लिखने से पहले" वाली टेबल के आकार में:
| केस | इनपुट | अपेक्षित | |---|---|---| | वैध notes रहते हैं | तीन notes, एक का title " " | फ़ाइल में बाकी दो, बिना बदलाव | | फ़ोल्डर मौजूद नहीं | tmp_path / "a" / "b" | दोनों फ़ोल्डर बनते हैं, फ़ाइल अंदर | | खाली title | वही तीन notes | एक WARNING, फिर INFO exported 2 notes | | आउटपुट | वही तीन notes | stdout पर saved notes.json\n, stderr पर कुछ नहीं | | एक्सपोर्ट को कुछ नहीं | [] | फ़ाइल में [] |
notes.py:
import json
import logging
logger = logging.getLogger("notes")
def export_notes(notes, out_dir):
"""Write notes with a title to out_dir/notes.json and return its path."""
out_dir.mkdir(parents=True, exist_ok=True)
valid = []
for note in notes:
if not note["title"].strip():
logger.warning("skipped a note with an empty title")
continue
valid.append(note)
target = out_dir / "notes.json"
target.write_text(json.dumps(valid, indent=2))
logger.info("exported %d notes", len(valid))
print("saved notes.json")
return targettest_notes.py:
import json
import logging
from notes import export_notes
NOTES = [
{"title": "Buy pens", "done": False},
{"title": " ", "done": True},
{"title": "Call the printer", "done": True},
]
def test_file_holds_only_valid_notes(tmp_path):
target = export_notes(NOTES, tmp_path)
assert target == tmp_path / "notes.json"
assert json.loads(target.read_text()) == [NOTES[0], NOTES[2]]
def test_creates_missing_folders(tmp_path):
out_dir = tmp_path / "a" / "b"
target = export_notes(NOTES, out_dir)
assert target.parent == out_dir
assert target.exists()
def test_empty_title_is_warned_about(tmp_path, caplog):
caplog.set_level(logging.INFO, logger="notes")
export_notes(NOTES, tmp_path)
assert [(r.levelname, r.getMessage()) for r in caplog.records] == [
("WARNING", "skipped a note with an empty title"),
("INFO", "exported 2 notes"),
]
def test_prints_one_line_and_no_errors(tmp_path, capsys):
export_notes(NOTES, tmp_path)
out, err = capsys.readouterr()
assert out == "saved notes.json\n"
assert err == ""
def test_no_notes_still_writes_an_empty_list(tmp_path):
target = export_notes([], tmp_path)
assert json.loads(target.read_text()) == []$ pytest -q
..... [100%]
5 passed in 0.10s
$ ls
notes.py test_notes.pyहर फ़ैसले का कारण:
out_dirएक parameter है। यही हर टेस्ट कोtmp_pathदेने देता है; आख़िरीlsदिखाता है कि प्रोजेक्ट में कोईnotes.jsonनहीं है।- JSON टेस्ट फ़ाइल को
json.loadsसे वापस पढ़ता है, टेक्स्ट की तुलना करने के बजाय। वादा है "ये notes, डेटा के रूप में"; सटीक indentationjson.dumpsका मामला है, हमारा नहीं।NOTES[0]औरNOTES[2]से तुलना यह भी साबित करती है कि वैध notes बिना बदलाव लिखे गए। - title
" "स्पेस है,""नहीं। सिर्फ़ स्पेस वाला title boundary केस है —if not note["title"]को वह खाली नहीं लगता, सिर्फ़.strip()उसे पकड़ता है। टेस्ट उस फ़ैसले को पक्का कर देता है। caplog.set_levelपहले आता है, और"notes"को निशाना बनाता है। इसके बिनाINFOलाइन कभी कैप्चर नहीं होती।(levelname, message)जोड़ियों की तुलना एक ही assert में लेवल, शब्द और क्रम जाँच लेती है —caplog.textको छुए बिना, जिसके फ़ाइल नाम और लाइन नंबर हर एडिट पर टेस्ट तोड़ देते।err == ""जानबूझकर assert किया गया है। "कुछ गलत नहीं हुआ" भी अनुबंध का हिस्सा है, और वरना standard error पर एक भटका हुआ debugprintनिकल जाता।- खाली लिस्ट का अपना टेस्ट है। नए अकाउंट पर फ़ाइल का पाठक सबसे पहले इसी edge case से टकराएगा, और कुछ न लिखने की जगह
[]लिखना एक फ़ैसला है जिसे बनाए रखना चाहिए।
Step 6 of 6
चुनौती — the chapter quiz
सरल से कठिन — दस प्रश्न, अंतिम वाले जानबूझकर चुनौतीपूर्ण बनाए गए हैं।
Sign in to take the quiz