2025, Oct 30 17:32
Pytest में class inheritance के साथ फ़िक्स्चर को सुरक्षित रूप से extend करने का भरोसेमंद तरीका
Pytest फ़िक्स्चर को override नहीं, extend करना है? class inheritance में recursive dependency त्रुटि से बचें: base फ़िक्स्चर का alias बनाकर भरोसेमंद समाधान पाएं.
Pytest फ़िक्स्चर को बढ़ाना एक आम ज़रूरत है: हर बार फ़िक्स्चर को ओवरराइड करना आवश्यक नहीं होता—कभी-कभी आप मूल फ़िक्स्चर को ही दुबारा इस्तेमाल करके उसके ऊपर अपना डेटा जोड़ना चाहते हैं। साधारण स्थिति में यह ठीक चलता है, लेकिन class-आधारित inheritance में टूट जाता है, जब subclass का कोई फ़िक्स्चर base class में उसी नाम वाले फ़िक्स्चर को कॉल करने की कोशिश करता है। नतीजा होता है recursive dependency की त्रुटि। नीचे इस समस्या की व्यवहारिक व्याख्या और उसका एक छोटा, भरोसेमंद उपाय दिया है।
उसी class के अंदर फ़िक्स्चर का पुन: उपयोग
एक सीधा उदाहरण इस तरह दिखता है। एक फ़िक्स्चर एक आधार सूची लौटाता है, और उसी नाम वाला दूसरा फ़िक्स्चर उसे इनपुट के रूप में लेता है और कुछ और मान जोड़ देता है। इसके बाद टेस्ट को संयुक्त डेटा मिलता है:
import pytest
@pytest.fixture
def numbers():
    return [1, 2, 3]
class Suite:
    @pytest.fixture
    def numbers(self, numbers):
        return numbers + [4, 5, 6]
    def test_numbers(self, numbers):
        assert 1 in numbers
        assert 2 in numbers
        assert 3 in numbers
        assert 4 in numbers
        assert 5 in numbers
        assert 6 in numbers
यह पैटर्न इसलिए काम करता है क्योंकि class के अंदर वाला फ़िक्स्चर, मॉड्यूल-स्तर पर परिभाषित बाहरी फ़िक्स्चर पर निर्भर करता है; नाम resolve करते समय कोई अस्पष्टता नहीं रहती।
कहां टूटता है: class inheritance और recursive dependency
अब मान लें कि आप inherited class में base variant को कॉल करके class-स्तरीय फ़िक्स्चर को बढ़ाना चाहते हैं। इरादा वही है, लेकिन नाम-निर्णयन अलग तरह से होता है और आप recursion के जाल में फँस जाते हैं:
import pytest
class ParentSuite:
    @pytest.fixture
    def numbers(self):
        return [1, 2, 3]
class ChildSuite(ParentSuite):
    @pytest.fixture
    def numbers(self, numbers):
        return numbers + [4, 5, 6]
    def test_numbers(self, numbers):
        assert 1 in numbers
        assert 2 in numbers
        assert 3 in numbers
        assert 4 in numbers
        assert 5 in numbers
        assert 6 in numbers
यहां Pytest यह बताता है:
E recursive dependency involving fixture 'values' detected
दूसरी फ़िक्स्चर परिभाषा उसी नाम के parent फ़िक्स्चर को “देख” नहीं पाती। Python में function overloading नहीं है, और subclass के अंदर नाम सबसे लोकल फ़िक्स्चर पर resolve होता है—जो आगे चलकर खुद को ही मांग लेता है, और recursion पैदा हो जाता है।
सिर्फ base फ़िक्स्चर का नाम बदलना समाधान क्यों नहीं है
बेस फ़िक्स्चर का नाम बदलना आसान रास्ता लग सकता है, लेकिन यह उन परिस्थितियों को तोड़ देता है जहां साझा टेस्ट किसी hierarchy में एक ही फ़िक्स्चर नाम को टार्गेट करते हैं। मान लें कि एक पुन: प्रयोज्य टेस्ट base class पर परिभाषित है, और दो subclasses उसी डेटा सेट को अलग-अलग तरीके से बढ़ाती हैं और साथ ही एक अलग फ़िक्स्चर को parametrize भी करती हैं:
import pytest
class ParentSuite:
    @pytest.fixture
    def numbers(self):
        return [1, 2, 3]
    def test_contains(numbers, item):
        assert item in numbers
class SuiteAlpha(ParentSuite):
    @pytest.fixture
    def numbers(self, numbers):
        return numbers + [4, 5, 6]
    @pytest.fixture(params=[1, 2, 3, 4, 5, 6])
    def item(self):
        return request.param
class SuiteBeta(ParentSuite):
    @pytest.fixture
    def numbers(self, numbers):
        return numbers + [7, 8, 9]
    @pytest.fixture(params=[1, 2, 3, 7, 8, 9])
    def item(self):
        return request.param
यह साझा टेस्ट numbers नाम के फ़िक्स्चर पर निर्भर है। अगर आप base फ़िक्स्चर का नाम बदलते हैं, तो या तो आपको सभी shared tests में बदलाव करना पड़ेगा, या हर जगह अतिरिक्त indirection जोड़नी होगी—जो साफ-सुथरे पुन: उपयोग के उद्देश्य को कमजोर कर देता है।
कारगर उपाय: base method का alias बनाएं और उसी पर निर्भर करें
विश्वसनीय तरीका यह है कि base class के फ़िक्स्चर का एक method alias बना लें और subclass में उसी alias पर निर्भर करें। इससे सार्वजनिक फ़िक्स्चर नाम ज्यों का त्यों रहता है, जबकि subclass भीतर से base के ऊपर निर्माण कर पाती है:
import pytest
class ParentSuite:
    @pytest.fixture
    def numbers(self):
        return [1, 2, 3]
class ChildSuite(ParentSuite):
    __seed_numbers = ParentSuite.numbers
    @pytest.fixture
    def numbers(self, __seed_numbers):
        return __seed_numbers + [4, 5, 6]
    def test_numbers(self, numbers):
        assert 1 in numbers
        assert 2 in numbers
        assert 3 in numbers
        assert 4 in numbers
        assert 5 in numbers
        assert 6 in numbers
यह alias डेकोरेटर्स का सम्मान करता है, और Pytest subclass वाले फ़िक्स्चर से base class वाले फ़िक्स्चर तक निर्भरता बना देता है। टेस्टों के लिए subclass अब भी numbers ही प्रस्तुत करती है, लेकिन अंदरखाने वह aliased base implementation पर आधारित रहती है।
यह क्यों मायने रखता है
टेस्ट सूट अक्सर साझा व्यवहार के लिए inheritance पर निर्भर रहते हैं, और फ़िक्स्चर भी उसी अनुबंध का हिस्सा होते हैं। जब कोई पुन: प्रयोज्य टेस्ट किसी फ़िक्स्चर नाम का संदर्भ देता है, तो subclasses को चाहिए कि वे उसी नाम के फ़िक्स्चर को परिष्कृत कर सकें—बिना टेस्ट फिर से लिखे या एक ही अवधारणा के लिए कई नाम रखने की मजबूरी के। Recursive dependency से बचना ज़रूरी है, ताकि test composition पूर्वानुमेय रहे और fragile वर्कअराउंड पूरे कोडबेस में न फैलें।
मुख्य निष्कर्ष
अगर आपको एक ही नाम के class-स्तरीय फ़िक्स्चर को बदलने के बजाय बढ़ाना है, तो base फ़िक्स्चर का method alias बनाएं और derived फ़िक्स्चर को उसी alias पर निर्भर कर दें। इससे recursive dependency से बचाव होता है, साझा टेस्टों के लिए फ़िक्स्चर का सार्वजनिक नाम बरकरार रहता है, और आपकी टेस्ट hierarchy सुसंगत व संभालने में आसान रहती है।
यह लेख StackOverflow पर प्रश्न (लेखक: Dmitry Kuzminov) और उसी के उत्तर पर आधारित है।