2025, Oct 20 10:31

pytest में Python CLI का विश्वसनीय परीक्षण: पैकेज-आधारित तरीका

Python CLI को pytest में भरोसेमंद ढंग से टेस्ट करें: subprocess चलाते समय ModuleNotFoundError से बचने के लिए पैकेज संरचना अपनाएँ, साझा कोड अलग करें.

Python में CLI का विश्वसनीय परीक्षण अक्सर तब अटक जाता है, जब किसी टेस्ट को subprocess के ज़रिए उसी स्क्रिप्ट को चलाते हुए टॉप-लेवल स्क्रिप्ट से कुछ प्रतीकों (symbols) को इम्पोर्ट करना पड़ता है। इसका आम लक्षण pytest में ModuleNotFoundError है, जब स्क्रिप्ट के मॉड्यूल पथ से इम्पोर्ट करने की कोशिश की जाती है। समस्या pytest में नहीं, बल्कि प्रोजेक्ट की संरचना और Python के इम्पोर्ट रिज़ॉल्यूशन के तरीके में है।

समस्या को पुन: उत्पन्न करने वाला न्यूनतम सेटअप

यह लेआउट देखें:

SomeDir
|- script.py
|- TestDir
|  |- test.py

स्क्रिप्ट एक Enum के माध्यम से exit codes उपलब्ध कराती है और sys.exit के साथ समाप्त होती है। टेस्ट, स्क्रिप्ट को subprocess के रूप में चलाते हैं और returncode को उसी Enum से मिलाकर परखते हैं। लेकिन टेस्ट के भीतर सीधे स्क्रिप्ट से Enum इम्पोर्ट करना असफल हो जाता है, क्योंकि टॉप-लेवल डायरेक्टरी पैकेज की तरह इम्पोर्ट करने योग्य नहीं है।

नीचे विफलता की स्थिति दिखाने वाला संक्षिप्त उदाहरण है:

# SomeDir/script.py
from enum import Enum
import sys
class ExitSignal(Enum):
    OK = 0
    FAIL = 1
class Runner:
    def execute(self):
        # कुछ प्रोसेसिंग का सिमुलेशन
        sys.exit(ExitSignal.OK.value)
if __name__ == '__main__':
    Runner().execute()
# SomeDir/TestDir/test.py
import subprocess
from script import ExitSignal  # TestDir से चलाने पर ModuleNotFoundError
def test_cli_exit_status():
    completed = subprocess.run(['python', '../script.py'], capture_output=True)
    assert completed.returncode == ExitSignal.OK.value

इम्पोर्ट क्यों विफल होता है

जब pytest को tests डायरेक्टरी के अंदर से चलाया जाता है, Python पैरेंट डायरेक्टरी को पैकेज नहीं मानता। इंटरप्रेटर from script import ExitSignal को रिज़ॉल्व नहीं कर पाता, क्योंकि पैरेंट फ़ोल्डर sys.path पर एक इम्पोर्टेबल पैकेज के रूप में मौजूद नहीं होता, और उसमें वह पैकेजिंग संकेत भी नहीं होता जो absolute या package-relative इम्पोर्ट की अनुमति देता है। sys.path में बदलाव जैसे त्वरित उपाय काम करते हुए लग सकते हैं, पर वे नाज़ुक होते हैं और आसानी से गलत हो सकते हैं।

मज़बूत तरीका: डायरेक्टरी को पैकेज बनाएं और पुन: प्रयोज्य लॉजिक अलग करें

बेहतर समाधान यह है कि टॉप‑लेवल डायरेक्टरी को एक सही पैकेज में बदला जाए और साझा हिस्सों—जैसे exit codes वाला Enum—को अलग मॉड्यूल में स्थानांतरित किया जाए। CLI एक हल्का एंट्री पॉइंट बना रहता है, जबकि टेस्ट और CLI दोनों उसी साझा मॉड्यूल को इम्पोर्ट करते हैं। इम्पोर्ट्स लगातार सही रिज़ॉल्व हों, इसके लिए pytest को पैकेज की रूट से चलाएँ।

प्रोजेक्ट को इस तरह पुनर्संरचित करें:

SomeDir/
│
├── script.py               # CLI एंट्री पॉइंट
├── common.py               # साझा कोड: exit codes आदि
├── __init__.py             # इस डायरेक्टरी को पैकेज के रूप में चिह्नित करता है
│
└── TestDir/
    └── test.py             # pytest टेस्ट्स

साझा लॉजिक common.py में जाएगा:

# SomeDir/common.py
from enum import Enum
class ExitSignal(Enum):
    OK = 0
    FAIL = 1

CLI एंट्री पॉइंट साझा मॉड्यूल से इम्पोर्ट करता है और उचित कोड के साथ बाहर निकलता है:

# SomeDir/script.py
import sys
from common import ExitSignal
def main():
    # कुछ प्रोसेसिंग का सिमुलेशन
    sys.exit(ExitSignal.OK.value)
if __name__ == '__main__':
    main()

टेस्ट उसी साझा मॉड्यूल से वही symbols इम्पोर्ट करते हैं और subprocess के एग्ज़िट स्टेटस की पुष्टि करते हैं:

# SomeDir/TestDir/test.py
import subprocess
from common import ExitSignal
def test_script_success():
    outcome = subprocess.run(['python', '../script.py'], capture_output=True)
    assert outcome.returncode == ExitSignal.OK.value

पैकेज खोजने योग्य रहे, इसके लिए टेस्ट टॉप‑लेवल डायरेक्टरी से चलाएँ:

cd SomeDir
pytest

यह पैटर्न क्यों महत्वपूर्ण है

यह तरीका sys.path को छेड़े बिना इम्पोर्ट्स को पूर्वानुमेय बनाता है, CLI को पुन: प्रयोज्य लॉजिक से मुक्त रखता है, और सुनिश्चित करता है कि एग्ज़िट कोड्स के लिए एप्लिकेशन और टेस्ट दोनों एक ही स्रोत पर निर्भर हों। साझा परिभाषाओं को पैकेज के मॉड्यूल में केंद्रीकृत करके आप नाम टकरावों से बचते हैं और ऐसे नाज़ुक उपायों से दूर रहते हैं जो current working directory या execution context पर निर्भर हों।

व्यावहारिक निष्कर्ष और सुझाव

प्रोजेक्ट को पैकेज के रूप में व्यवस्थित करें, Enum जैसी साझा चीज़ों को अलग मॉड्यूल में रखें, script.py को एंट्री पॉइंट रहने दें, और pytest को पैकेज रूट से चलाएँ। इससे इम्पोर्ट्स स्थिर रहते हैं, CLI और लाइब्रेरी कोड के बीच स्पष्ट सीमाएँ बनती हैं, और tests में बिना sys.path बदले या तात्कालिक वर्कअराउंड पर निर्भर हुए सरल पुन: उपयोग संभव होता है।

यह लेख StackOverflow पर प्रश्न (लेखक: Oersted) और John Eipe के उत्तर पर आधारित है।