अध्याय 09

built-in fixtures — tmp_path, capsys और caplog

टेस्टिंग की झंझट वाली जगहों के लिए pytest के साथ आने वाले fixtures: फ़ाइलें, प्रिंट आउटपुट और लॉग। असली paths को छुए बिना CSV और JSON लिखने वाले कोड को टेस्ट करना, और यह जाँचना कि वह क्या प्रिंट और लॉग करता है।

45 मिनटPython 3.12
  1. 1समस्या
  2. 2समझें
  3. 3उदाहरण
  4. 4अनुमान
  5. 5स्वयं करें
  6. 6चुनौती

वह समस्या जिसे हम हल कर रहे हैं

यहाँ एक फ़ंक्शन है जो एक CSV फ़ाइल लिखता है, और उसके लिए एक टेस्ट:

python
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"
text
$ 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 आपके टेस्ट को एक ऐसी डायरेक्टरी देता है जो एक पल पहले मौजूद नहीं थी:

python
def test_first(tmp_path):
    print(tmp_path)
    print(type(tmp_path))


def test_second(tmp_path):
    print(tmp_path)
text
$ 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 फ़ंक्शन की एक जोड़ी, क्योंकि सेटिंग्स सेव करना लगभग हर प्रोग्राम का दूसरा फ़ाइल-लेखन काम है:

python
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()
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 के ज़रिए फ़ोल्डर माँगते हैं:

python
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
text
$ 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 — प्रोग्राम क्या प्रिंट करता है, यह टेस्ट करना

उस गलती से शुरू करते हैं जो लगभग हर कोई एक बार करता है। एक फ़ंक्शन अभिवादन प्रिंट करता है; टेस्ट उसका नतीजा जाँचता है:

python
def greet(name):
    print(f"hello, {name}")


def test_greet():
    assert greet("Rina") == "hello, Rina"
text
$ 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.09s

print टेक्स्ट भेजता है; लौटाता नहीं, इसलिए greet लौटाता है None। लेकिन रिपोर्ट के नीचे देखिए: Captured stdout call — pytest ने टेक्स्ट पहले ही पकड़ लिया है। टर्मिनल साफ़ रखने के लिए वह हर टेस्ट में ऐसा करता है। capsys उसे हाथ में लेने का तरीका है:

python
def greet(name):
    print(f"hello, {name}")


def test_greet(capsys):
    greet("Rina")

    assert capsys.readouterr().out == "hello, Rina\n"
text
$ pytest -q
.                                                                        [100%]
1 passed in 0.07s

पहले फ़ंक्शन बुलाइए, फिर पढ़िए उसने क्या प्रिंट किया। असली कमांड-लाइन प्रोग्राम एक से ज़्यादा लाइनें, दो streams में प्रिंट करते हैं, और एक exit code लौटाते हैं। यह रहा एक, टेस्ट के साथ:

python
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
text
$ pytest -q
...                                                                      [100%]
3 passed in 0.08s

capsys.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 पर कुछ नहीं गया।

"पिछली बार माँगने के बाद से" शब्दशः है — पढ़ने से बफ़र खाली हो जाता है:

python
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"
text
$ pytest -q
.                                                                        [100%]
1 passed in 0.07s

यह काम का है: आप आउटपुट कदम-दर-कदम जाँच सकते हैं। यही कारण भी है कि दूसरा readouterr() कभी-कभी अचानक खाली लौट आता है।

capfd — जब आउटपुट पायथन से होकर नहीं जाता

capsys पायथन के sys.stdout और sys.stderr को बदल देता है। जो कुछ सीधे ऑपरेटिंग सिस्टम के आउटपुट पर लिखता है — एक child process, एक C extension — वह इसके बगल से निकल जाता है। capfd एक स्तर नीचे, file descriptor पर पकड़ता है, और उसे भी पकड़ लेता है:

python
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"
text
$ pytest -q
..                                                                       [100%]
2 passed in 0.07s

वही readouterr(), वही .out और .err। डिफ़ॉल्ट रूप से capsys इस्तेमाल कीजिए और capfd पर तभी जाइए जब पायथन के बाहर का आउटपुट गायब हो।

caplog — प्रोग्राम क्या लॉग करता है, यह टेस्ट करना

असली प्रोग्राम प्रिंट करने के बजाय लॉग करते हैं। लॉग में एक चेतावनी भी किसी और व्यवहार जैसी ही है, इसलिए वह भी एक टेस्ट की हक़दार है:

python
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
text
$ pytest -q -s
."WARNING  importer:test_importer.py:11 skipped 'bag': bad quantity\n"
.
2 passed in 0.07s

caplog आपको वही लॉग तीन तरह से देता है:

  • caplog.records — logging.LogRecord ऑब्जेक्ट्स की लिस्ट, हर मैसेज का एक। हर एक के पास है .levelname ("WARNING"), .name (logger, "importer"), और .getMessage(), जो %r की जगह उसकी वैल्यू भर देता है।
  • caplog.messages — सिर्फ़ तैयार मैसेज strings, एक लिस्ट के रूप में।
  • caplog.text — सब कुछ एक फ़ॉर्मैट की हुई string के रूप में। जल्दी से in जाँच के लिए अच्छा; किसी भी सटीक चीज़ के लिए बहुत ढीला।

जब लेवल मायने रखता हो तो records चुनिए, और जब सिर्फ़ शब्द मायने रखते हों तो messages।

WARNING से नीचे के मैसेज डिफ़ॉल्ट रूप से कैप्चर नहीं होते

parse_quantities एक INFO सारांश भी लॉग करता है। उसके लिए एक टेस्ट जोड़िए:

python
def test_summary_is_logged(caplog):
    parse_quantities(["pen,12"])

    assert caplog.messages == ["parsed 1 of 1 lines"]
text
$ 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 से नीचे कीजिए:

python
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"]
text
$ pytest -q
..                                                                       [100%]
2 passed in 0.07s

set_level को लॉग करने वाले कोड से पहले बुलाइए। दूसरा रूप, logger="importer" के साथ, सिर्फ़ उस एक logger का लेवल नीचे करता है, ताकि बातूनी लाइब्रेरी चुप रहें। दोनों हालात में, टेस्ट ख़त्म होने पर pytest पुराना लेवल वापस रख देता है।

request और pytestconfig

एक और built-in fixture दूसरे fixtures के अंदर दिखता है। request उस टेस्ट का ब्योरा देता है जिसने fixture माँगा; request.node.name उस टेस्ट का नाम है। फ़ाइलों को पढ़ने लायक, अनोखे नाम देने के लिए यह काम आता है:

python
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"))
text
$ pytest -q -s
test_pens.csv
.test_bulk[1].csv
.test_bulk[50].csv
.shop -1
.
4 passed in 0.07s

parametrize किए गए टेस्ट के नाम में उसके parameters वर्ग कोष्ठक में होते हैं। और आख़िरी टेस्ट pytestconfig दिखाता है: रन का कॉन्फ़िगरेशन ऑब्जेक्ट, जिसमें प्रोजेक्ट का root फ़ोल्डर और हर कमांड-लाइन ऑप्शन होता है (-q verbosity को -1 कर देता है)। कॉन्फ़िगरेशन और plugins वाले अध्यायों में इससे फिर मुलाकात होगी।


पूर्ण उदाहरण

export.py वैध स्टॉक पंक्तियाँ एक CSV में लिखता है, हर छोड़ी गई पंक्ति के लिए एक चेतावनी लॉग करता है, और एक सारांश प्रिंट करता है:

python
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 target

test_export.py उसके तीनों आउटपुट जाँचता है — फ़ाइल, लॉग और टर्मिनल:

python
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"
text
$ 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 को अलग-अलग स्तर पर पकड़ते हैं; एक चुनिए।