2025, Oct 21 08:32
pytest में फ़ाइल I/O टेस्ट: open मंकीपैच की समस्या और बेहतर विकल्प
pytest में open को मंकीपैच करने से pdb में encoding वाला TypeError उठ सकता है। जानें क्यों, और फ़ाइल I/O टेस्ट के लिए tmp_path व ओपनर इंजेक्शन के सुरक्षित तरीके।
जब कोई फ़ंक्शन फ़ाइल पढ़ता है, तो open जैसे बिल्ट‑इन को मंकीपैच करना आकर्षक लग सकता है, लेकिन डिबगिंग के दौरान यह अक्सर उल्टा पड़ता है। इसका एक सामान्य लक्षण है pdb शुरू होते ही encoding नाम के अप्रत्याशित कीवर्ड आर्गुमेंट पर TypeError। यहाँ समस्या की जड़ और यह कि डिबगर से टकराए बिना फ़ाइल I/O का परीक्षण कैसे करें—दोनों पर बात करते हैं।
न्यूनतम उदाहरण
यह फ़ंक्शन फ़ाइल पढ़कर अद्वितीय अक्षरात्मक वर्णों की गिनती करता है। टेस्ट builtins.open को एक लैम्ब्डा से बदल देता है, जो इन‑मेमोरी स्ट्रीम लौटाता है। जैसे ही pdb शामिल होता है, टेस्ट TypeError के साथ फट पड़ता है।
import builtins
import io
import pytest
def tally_unique_alpha_from_file(p: str) -> int:
    with open(p) as fh:
        return len(set(ch.lower() for ch in fh.read() if ch.isalpha()))
def test_counts(monkeypatch: pytest.MonkeyPatch):
    src = "Quick frog or something"
    monkeypatch.setattr(builtins, "open", lambda _p: io.StringIO(src))
    assert tally_unique_alpha_from_file("blah.txt") == 15
यह क्यों टूटता है
लैम्ब्डा वाला प्रतिस्थापन सिर्फ एक पोज़िशनल आर्गुमेंट स्वीकार करता है। असली open कई पैरामीटर को सपोर्ट करता है, जिनमें encoding भी शामिल है। जब आप pdb में जाते हैं (जैसे pytest --pdb या pdb.set_trace() के जरिए), तो pdb स्वयं अपनी rc फ़ाइल, जैसे ~/.pdbrc, पढ़ने के लिए स्पष्ट encoding के साथ open को कॉल करता है। वह कॉल लैम्ब्डा पर गिरती है, जो encoding नहीं लेता, और Python TypeError उठा देता है। संक्षेप में, टेस्ट ने डिबगर द्वारा इस्तेमाल किए जाने वाले एक वैश्विक प्रिमिटिव को बदल दिया, और नतीजा यह हुआ कि असंबंधित टूलिंग टूटने लगी।
बेहतर तरीका: वास्तविक अस्थायी फ़ाइल का उपयोग करें
फ़ाइल पढ़ने वाले कोड का परीक्षण करने के लिए आपको किसी चीज़ को मॉक या मंकीपैच करने की ज़रूरत नहीं है। Pytest अस्थायी डिरेक्टरीज़ के लिए tmp_path देता है। एक वास्तविक फ़ाइल बनाएँ, आवश्यक सामग्री लिखें, और उसका पथ फ़ंक्शन को दें। ऐसा करने से डिबगर और बाकी रनटाइम अप्रभावित रहते हैं।
import pathlib
def tally_unique_alpha_from_file(p: str) -> int:
    with open(p) as fh:
        return len(set(ch.lower() for ch in fh.read() if ch.isalpha()))
def test_counts(tmp_path: pathlib.Path):
    fpath = tmp_path / "blah.txt"
    fpath.write_text("Quick frog or something")
    assert tally_unique_alpha_from_file(str(fpath)) == 15
डिफ़ॉल्ट रूप से pytest साफ़ करने से पहले पिछली तीन टेस्ट रन की डिरेक्टरी बचाए रखता है, और यदि कोई टेस्ट विफल होता है तो आउटपुट में आपको फ़िक्स्चर के मान दिखते हैं—जिससे डिबगिंग आसान हो जाती है। यदि आप स्टैंडर्ड लाइब्रेरी टूल्स पसंद करते हैं, तो अस्थायी फ़ाइल के साथ एक इंटीग्रेशन टेस्ट भी उपयुक्त है।
वैकल्पिक तरीका: ओपनर इंजेक्ट करें
यदि आप builtins को छुए बिना यह नियंत्रित करना चाहते हैं कि फ़ाइलें कैसे खुलें, तो निर्भरता उलट दें और एक opener फ़ंक्शन स्वीकार करें। फ़ंक्शन डिफ़ॉल्ट रूप से अभी भी open का उपयोग करता है, लेकिन टेस्ट अलग callable दे सकते हैं। इससे अनुबंध स्पष्ट होता है और वैश्विक साइड इफेक्ट्स से बचाव होता है।
from collections.abc import Callable
from io import StringIO
from typing import TextIO
from unittest.mock import Mock
def tally_unique_alpha_from_file(
    p: str, *, open_fn: Callable[[str], TextIO] = open
) -> int:
    with open_fn(p) as fh:
        return len(set(ch.lower() for ch in fh.read() if ch.isalpha()))
def test_counts_with_mock():
    fake_open = Mock(spec=open)
    fake_open.return_value = StringIO("Quick frog or something")
    assert tally_unique_alpha_from_file("blah.txt", open_fn=fake_open) == 15
यह मॉडल निर्भरता को उलट देता है और इंटरफ़ेस को भी स्पष्ट करता है, यह बताते हुए कि फ़ंक्शन ओपनर का उपयोग किस तरह करने वाला है। न मंकीपैचिंग, न pdb या अन्य टूलिंग के लिए कोई अप्रत्याशित समस्या।
यह क्यों मायने रखता है
जिस पर आपका नियंत्रण नहीं है, खासकर builtins, उसे मॉक करना रनटाइम के असंबंधित हिस्सों—जैसे डिबगर, टेस्ट हार्नेस, या वे लाइब्रेरी जो मानक व्यवहार पर निर्भर हैं—को तोड़ सकता है। अस्थायी पथों के जरिए वास्तविक फ़ाइलों का उपयोग करने वाले टेस्ट अधिक मज़बूत और समझने में आसान होते हैं। एक सावधानी यह है कि अस्थायी फ़ाइलें यह नहीं बतातीं कि फ़ाइल कितनी बार खोली गई, जो कैशिंग सत्यापित करने के लिए मायने रख सकता है; लेकिन अधिकांश I/O टेस्ट इनके साथ सरल और अधिक भरोसेमंद बनते हैं।
सारांश
यदि आपको सिर्फ फ़ाइल की सामग्री चाहिए, तो pytest के tmp_path या किसी वास्तविक अस्थायी फ़ाइल का सहारा लें। और अगर यह देखना या नियंत्रित करना ज़रूरी है कि फ़ाइल कैसे खोली जाए, तो builtins बदलने की बजाय ओपनर इंजेक्ट करें। दोनों ही तरीकों में आप pdb के साथ आराम से डिबग कर सकते हैं, बिना उसके भीतर होने वाली encoding‑संबंधी कॉल्स से टकराए।
यह लेख StackOverflow पर प्रश्न (लेखक: Dominik Kaszewski) और jonrsharpe के उत्तर पर आधारित है।