2025, Nov 02 05:32

pytest फ़िक्स्चर से महँगी गणना का ऑटो-कैश: पिकल आधारित तरीका

pytest फ़िक्स्चर से पहले रन पर महँगी गणना कर परिणाम पिकल में सहेजें, और अगले रन में वही डेटा कैश से लोड करें। प्रोजेक्ट रूट पाथ व session स्कोप के साथ भरोसेमंद टेस्ट.

महँगी गणना को पहले से कर लेना और उसे हर टेस्ट रन में दोबारा इस्तेमाल करना फीडबैक लूप को फुर्तीला रखने का व्यावहारिक तरीका है। लेकिन अड़चन यह है: जब आप पहले रन के लिए मैनुअल इनिशियलाइज़र पर निर्भर रहते हैं, तो टेस्ट ऑटोमेशन नाज़ुक हो जाता है। Pytest के साथ आपको कोड को कभी ऑन, कभी ऑफ करने की जरूरत नहीं; फ़िक्स्चर पहली बार डेटा पैदा कर के उसे सहेज सकता है, और अगले रन में वही डेटा सीधे उपयोग हो जाता है।

मैनुअल तरीके में दिक्कत कहाँ आती है

शुरुआत में वर्कफ़्लो सीधा लगता है: भारी सॉल्वर एक बार चलाएँ, नतीजे को पिकल कर लें, फिर टेस्ट में उसे लोड करें। पर जैसे ही आपको याद रखना पड़ता है कि पिकल को पहले हाथ से “प्राइम” करना है, या रिलेटिव पाथ की वजह से pytest अलग-अलग जगह से चलाने पर डुप्लीकेट फाइलें बनने लगती हैं, चीजें बिगड़ जाती हैं। आपको ऐसा फ़र्स्ट-रन सेटअप चाहिए जो अपने-आप चले और लोकली सेशनों के बीच बना रहे।

दिक्कत दिखाने वाला छोटा उदाहरण

यहाँ उसी पैटर्न का संक्षिप्त उदाहरण है, जहाँ भारी गणना एक बार की जाती है और बाद में उसका पुनः उपयोग किया जाता है।

import time
import pickle
class Answer:
    """Represents a solution produced by a time-consuming routine."""
def crunch_hard_task() -> Answer:
    time.sleep(1)
    return Answer()
def snapshot_result() -> None:
    """Run the solver once and persist the outcome for feature development."""
    outcome = crunch_hard_task()
    with open('solved.pickle', 'wb') as fh:
        pickle.dump(outcome, fh)
def feature_under_test(solved: Answer) -> None:
    """Use the prepared solution."""
    print(solved)
def test_feature() -> None:
    # snapshot_result()  # इसे पहली बार चलाने पर हाथ से टॉगल करना पड़ता है।
    with open('solved.pickle', 'rb') as fh:
        restored = pickle.load(fh)
    assert feature_under_test(restored) is None

समस्या की जड़

टेस्ट यह मानकर चलते हैं कि फाइल पहले से मौजूद है। अगर नहीं है, तो वे फेल हो जाते हैं या आपको इनिशियलाइज़र को ऑन करने के लिए मजबूर करते हैं। साथ ही, पिकल को रिलेटिव पाथ पर रखने से, pytest अलग-अलग सबडायरेक्टरी से चलाने पर फाइल कई जगह बन सकती है। यानी विश्वसनीय, ऑटोमेटेड फ़र्स्ट-रन और पर्सिस्टेड डेटा के लिए एक तयशुदा स्थान—दोनों की कमी है।

pytest फ़िक्स्चर से पहले रन को स्वचालित करना

Pytest फ़िक्स्चर कोई भी सेटअप लॉजिक चला सकते हैं। हमें यही चाहिए: जाँचें कि पिकल मौजूद है या नहीं; हो तो लोड करें, वरना एक बार गणना करें, सहेजें और वही ऑब्जेक्ट लौटाएँ। सबडायरेक्टरी से टेस्ट चलाने पर डुप्लीकेट फाइलों से बचने के लिए, पाथ को pytest के कॉन्फ़िगरेशन ऑब्जेक्ट के जरिए प्रोजेक्ट रूट से जोड़ें।

import pickle
from pathlib import Path
import pytest
class Answer:
    """Represents a solution produced by a time-consuming routine."""
def crunch_hard_task() -> Answer:
    # महँगी गणना का अनुकरण
    import time
    time.sleep(1)
    return Answer()
@pytest.fixture
def solved_artifact(pytestconfig) -> Answer:
    # प्रोजेक्ट रूट पर एक एकल, मानक स्थान बनाएँ
    pickle_file = pytestconfig.rootpath / "solved.pickle"
    if pickle_file.exists():
        with pickle_file.open("rb") as fh:
            return pickle.load(fh)
    print("Solving problem, please be patient")
    computed = crunch_hard_task()
    with pickle_file.open("wb") as fh:
        pickle.dump(computed, fh)
    return computed
def feature_under_test(solved: Answer) -> None:
    print(solved)
def test_feature(solved_artifact: Answer) -> None:
    assert feature_under_test(solved_artifact) is None

अब मैनुअल टॉगल की जरूरत नहीं। पहले रन पर फ़िक्स्चर पिकल बनाकर प्रोजेक्ट के बेस डायरेक्टरी में कैश कर देता है; बाद के रन में वही फाइल बस लोड हो जाती है। क्योंकि पाथ प्रोजेक्ट रूट से शुरू होता है, सबडायरेक्टरी से टेस्ट चलाने पर फाइलें कई जगह नहीं बनेंगी।

वैकल्पिक रूपांतर

यदि आप चाहते हैं कि गणना प्रत्येक pytest इन्वोकेशन में सिर्फ एक बार हो, भले ही कितने भी टेस्ट उसे इस्तेमाल करें, तो session-स्कोप वाला फ़िक्स्चर उपयोग करें। यह हर बार pytest शुरू होने पर चलता है, लेकिन प्रति सत्र केवल एक बार।

@pytest.fixture(scope="session")
def solved_artifact(pytestconfig) -> Answer:
    pickle_file = pytestconfig.rootpath / "solved.pickle"
    if pickle_file.exists():
        with pickle_file.open("rb") as fh:
            return pickle.load(fh)
    print("Solving problem, please be patient")
    computed = crunch_hard_task()
    with pickle_file.open("wb") as fh:
        pickle.dump(computed, fh)
    return computed

आपके डेटा मॉडल की स्थिरता पर निर्भर करते हुए, एक और व्यावहारिक तरीका यह है कि पिकल को सोर्स कंट्रोल में रखें और जरूरत पड़ने पर अलग से दोबारा बनाएँ। यह उपयुक्त है या नहीं, यह इस बात पर टिका है कि आधारभूत संरचना कितनी कम बदलती है।

Pytest में एक बिल्ट-इन कैश सिस्टम भी है जो बिना मैनुअली फाइल सँभाले रन के बीच डेटा बनाए रखने के लिए सीधे इस्तेमाल किया जा सकता है। कॉन्फ़िग कैश ऑब्जेक्ट के दस्तावेज़ देखें: https://docs.pytest.org/en/stable/how-to/cache.html#the-new-config-cache-object

यह क्यों मायने रखता है

पहले रन का सेटअप ऑटोमेट करने से टेस्ट रन निर्धार्य और दोहराने योग्य बनते हैं। यह इंसानी स्टेप हटाता है जिसे भूलना आसान होता है, और आर्टिफैक्ट्स को एक ही, अनुमानित जगह रखता है। जब टेस्ट कई सबपैकेजों में फैले हों और अलग-अलग वर्किंग डायरेक्टरी से चलाए जाएँ, तब यह और उपयोगी है; पर्सिस्टेड स्टेट के लिए प्रोजेक्ट रूट का उपयोग करने से बिखराव रुकता है।

मुख्य बातें

ऐसा pytest फ़िक्स्चर इस्तेमाल करें जो पर्सिस्टेड आर्टिफैक्ट की जाँच करे और ज़रूरत पड़ने पर उसे बनाए। स्टोरेज पाथ को pytest कॉन्फ़िगरेशन के जरिए प्रोजेक्ट रूट से बाँधें ताकि मौजूदा वर्किंग डायरेक्टरी चाहे जो हो, फाइल सभी टेस्ट इन्वोकेशनों में साझा रहे। यदि आपको प्रति-सेशन केवल एक बार चलना है, तो फ़िक्स्चर को session स्कोप दें; और अगर आप फाइलें मैनेज नहीं करना चाहते, तो pytest के cache सिस्टम का मूल्यांकन करें। और यदि आपका सोल्यूशन ऑब्जेक्ट बेहद स्थिर है, तो पिकल को वर्ज़न कंट्रोल में रखना भी सरल विकल्प हो सकता है। नतीजा: शून्य मैनुअल टॉगल, तेज़ इटरेशन, और एक पूर्वानुमेय, टिकाऊ सेटअप जो सेशनों के पार बना रहता है।

यह लेख StackOverflow के एक प्रश्न (लेखक: Paweł Wójcik) और David Maze के उत्तर पर आधारित है।