एक पैरामीटर को एक फंक्शन फ़ंक्शन में पास करें


114

मैं अजगर वर्ग MyTester में लिपटे कुछ DLL कोड का परीक्षण करने के लिए py.test का उपयोग कर रहा हूं। उद्देश्य को मान्य करने के लिए मुझे परीक्षणों के दौरान कुछ परीक्षण डेटा लॉग करने और बाद में अधिक प्रसंस्करण करने की आवश्यकता है। जैसा कि मेरे पास कई परीक्षण _.. फाइलें हैं जो मैं अपने अधिकांश परीक्षणों के लिए परीक्षक ऑब्जेक्ट निर्माण (MyTester का उदाहरण) का पुन: उपयोग करना चाहता हूं।

जैसा कि टेस्टर ऑब्जेक्ट वह है जिसे DLL के वेरिएबल्स और फ़ंक्शंस का संदर्भ मिला है, मुझे टेस्ट फाइल में से प्रत्येक के लिए टेस्टर ऑब्जेक्ट के लिए DLL के वेरिएबल्स की सूची को पास करने की आवश्यकता है (लॉग किए जाने वाले वेरिएबल्स test_ के लिए समान हैं।) । फ़ाइल)। निर्दिष्ट डेटा को लॉग करने के लिए सूची की सामग्री का उपयोग किया जाएगा।

मेरा विचार है कि इसे किसी तरह से करना है:

import pytest

class MyTester():
    def __init__(self, arg = ["var0", "var1"]):
        self.arg = arg
        # self.use_arg_to_init_logging_part()

    def dothis(self):
        print "this"

    def dothat(self):
        print "that"

# located in conftest.py (because other test will reuse it)

@pytest.fixture()
def tester(request):
    """ create tester object """
    # how to use the list below for arg?
    _tester = MyTester()
    return _tester

# located in test_...py

# @pytest.mark.usefixtures("tester") 
class TestIt():

    # def __init__(self):
    #     self.args_for_tester = ["var1", "var2"]
    #     # how to pass this list to the tester fixture?

    def test_tc1(self, tester):
       tester.dothis()
       assert 0 # for demo purpose

    def test_tc2(self, tester):
       tester.dothat()
       assert 0 # for demo purpose

क्या इसे इस तरह हासिल करना संभव है या फिर और भी खूबसूरत तरीका है?

आमतौर पर मैं इसे कुछ प्रकार के सेटअप फ़ंक्शन (xUnit-style) के साथ प्रत्येक परीक्षण विधि के लिए कर सकता था। लेकिन मैं किसी तरह का पुन: उपयोग करना चाहता हूं। क्या किसी को पता है कि क्या यह जुड़नार के साथ संभव है?

मुझे पता है कि मैं ऐसा कुछ कर सकता हूं: (डॉक्स से)

@pytest.fixture(scope="module", params=["merlinux.eu", "mail.python.org"])

लेकिन मुझे सीधे परीक्षण मॉड्यूल में पैरामीट्रिजेशन की आवश्यकता है। क्या परीक्षण मॉड्यूल से स्थिरता की पैरामेट्स विशेषता तक पहुंचना संभव है?

जवाबों:


101

अद्यतन: चूंकि यह इस प्रश्न का स्वीकृत उत्तर है और फिर भी कभी-कभी उठ जाता है, मुझे एक अद्यतन जोड़ना चाहिए। यद्यपि मेरा मूल उत्तर (नीचे) पाइस्टेस्ट के पुराने संस्करणों में ऐसा करने का एकमात्र तरीका था क्योंकि अन्य ने पहले से ही उल्लेख किया है कि अब जुड़नार के अप्रत्यक्ष पैराट्रिएजेशन का समर्थन करता है। उदाहरण के लिए आप ऐसा कुछ कर सकते हैं (@imiric के माध्यम से):

# test_parameterized_fixture.py
import pytest

class MyTester:
    def __init__(self, x):
        self.x = x

    def dothis(self):
        assert self.x

@pytest.fixture
def tester(request):
    """Create tester object"""
    return MyTester(request.param)


class TestIt:
    @pytest.mark.parametrize('tester', [True, False], indirect=['tester'])
    def test_tc1(self, tester):
       tester.dothis()
       assert 1
$ pytest -v test_parameterized_fixture.py
================================================================================= test session starts =================================================================================
platform cygwin -- Python 3.6.8, pytest-5.3.1, py-1.8.0, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: .
collected 2 items

test_parameterized_fixture.py::TestIt::test_tc1[True] PASSED                                                                                                                    [ 50%]
test_parameterized_fixture.py::TestIt::test_tc1[False] FAILED

हालाँकि, हालांकि अप्रत्यक्ष पैरामीरीज़ेशन का यह रूप स्पष्ट है, क्योंकि @Yukihiko Shinoda बताते हैं कि अब यह अप्रत्यक्ष अप्रत्यक्ष पैरामीज़ेशन के एक रूप का समर्थन करता है (हालाँकि मुझे आधिकारिक डॉक्स में इसके बारे में कोई स्पष्ट संदर्भ नहीं मिला):

# test_parameterized_fixture2.py
import pytest

class MyTester:
    def __init__(self, x):
        self.x = x

    def dothis(self):
        assert self.x

@pytest.fixture
def tester(tester_arg):
    """Create tester object"""
    return MyTester(tester_arg)


class TestIt:
    @pytest.mark.parametrize('tester_arg', [True, False])
    def test_tc1(self, tester):
       tester.dothis()
       assert 1
$ pytest -v test_parameterized_fixture2.py
================================================================================= test session starts =================================================================================
platform cygwin -- Python 3.6.8, pytest-5.3.1, py-1.8.0, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: .
collected 2 items

test_parameterized_fixture2.py::TestIt::test_tc1[True] PASSED                                                                                                                   [ 50%]
test_parameterized_fixture2.py::TestIt::test_tc1[False] FAILED

मुझे ठीक से पता नहीं है कि इस फॉर्म के शब्दार्थ क्या हैं, लेकिन ऐसा लगता है कि pytest.mark.parametrizeपहचानता है कि हालांकि test_tc1विधि एक तर्क का नाम नहीं लेती है tester_arg, वह जिस testerस्थिरता का उपयोग कर रही है, इसलिए यह जुड़ाव के माध्यम से पैराड्राइज्ड तर्क को पारित करता है tester


मुझे इसी तरह की समस्या थी - मेरे पास एक स्थिरता है जिसे बुलाया गया है test_package, और मैं बाद में विशिष्ट परीक्षणों में इसे चलाने पर उस स्थिरता के लिए एक वैकल्पिक तर्क पारित करने में सक्षम होना चाहता था। उदाहरण के लिए:

@pytest.fixture()
def test_package(request, version='1.0'):
    ...
    request.addfinalizer(fin)
    ...
    return package

(यह इन उद्देश्यों के लिए मायने नहीं रखता है कि स्थिरता क्या करती है या किस प्रकार की वस्तु लौटाती है package )।

तब यह किसी भी तरह से एक परीक्षण समारोह में इस स्थिरता का उपयोग करने के लिए वांछनीय होगा कि मैं भी निर्दिष्ट कर सकता हूं version उस परीक्षण के साथ उपयोग करने के लिए उस स्थिरता तर्क को सकूं। वर्तमान में यह संभव नहीं है, हालांकि एक अच्छी सुविधा हो सकती है।

इस बीच यह मेरी स्थिरता बनाने के लिए काफी आसान था, बस एक फ़ंक्शन वापस करें जो पहले किए गए सभी काम करता है, लेकिन मुझे versionतर्क निर्दिष्ट करने की अनुमति देता है:

@pytest.fixture()
def test_package(request):
    def make_test_package(version='1.0'):
        ...
        request.addfinalizer(fin)
        ...
        return test_package

    return make_test_package

अब मैं अपने परीक्षण कार्य में इसका उपयोग कर सकता हूं जैसे:

def test_install_package(test_package):
    package = test_package(version='1.1')
    ...
    assert ...

और इसी तरह।

ओपी का प्रयास समाधान सही दिशा में आगे बढ़ रहा था, और जैसा कि @ hpk42 के उत्तर से पता चलता है, MyTester.__init__बस अनुरोध के संदर्भ में स्टोर कर सकता है जैसे:

class MyTester(object):
    def __init__(self, request, arg=["var0", "var1"]):
        self.request = request
        self.arg = arg
        # self.use_arg_to_init_logging_part()

    def dothis(self):
        print "this"

    def dothat(self):
        print "that"

तो इस का उपयोग इस तरह स्थिरता को लागू करने के लिए:

@pytest.fixture()
def tester(request):
    """ create tester object """
    # how to use the list below for arg?
    _tester = MyTester(request)
    return _tester

यदि वांछित है तो MyTesterकक्षा को थोड़ा पुनर्गठित किया जा सकता है ताकि इसकी .argsविशेषता को बनाए जाने के बाद अद्यतन किया जा सके, व्यक्तिगत परीक्षणों के लिए व्यवहार को मोड़ने के लिए।


स्थिरता के अंदर फ़ंक्शन के साथ संकेत के लिए धन्यवाद। जब तक मैं इस पर फिर से काम कर सकता था तब तक कुछ समय लगा लेकिन यह बहुत उपयोगी है!
मैगी

2
इस विषय पर एक अच्छी छोटी पोस्ट: alysivji.github.io/pytest-fixures-with-function-arguments.html
maggie

क्या आपको यह कहते हुए एक त्रुटि नहीं मिल रही है: "फिक्स्चर को सीधे बुलाया नहीं जाता है, लेकिन स्वचालित रूप से बनाया जाता है जब परीक्षण फ़ंक्शन उन्हें मापदंडों के रूप में अनुरोध करते हैं।"
nz_21

153

यह वास्तव में परोक्ष रूप से पैरामीट्रिक के माध्यम से py.test में मूल रूप से समर्थित है ।

आपके मामले में, आपके पास होगा:

@pytest.fixture
def tester(request):
    """Create tester object"""
    return MyTester(request.param)


class TestIt:
    @pytest.mark.parametrize('tester', [['var1', 'var2']], indirect=True)
    def test_tc1(self, tester):
       tester.dothis()
       assert 1

आह, यह बहुत अच्छा है (मुझे लगता है कि आपका उदाहरण थोड़ा पुराना हो सकता है - हालांकि यह आधिकारिक डॉक्स में उदाहरणों से अलग है)। क्या यह अपेक्षाकृत नई सुविधा है? मैंने पहले कभी इसका सामना नहीं किया। यह समस्या का एक अच्छा समाधान है - कुछ मायनों में मेरे उत्तर से बेहतर है।
इगुआनाटुत

2
मैंने इस समाधान का उपयोग करने की कोशिश की, लेकिन कई मापदंडों को पारित करने या अनुरोध के अलावा अन्य चर नामों का उपयोग करने में समस्या हो रही थी। मैं @Iganaanaut समाधान का उपयोग कर समाप्त हुआ।
विक्टर उरिएर्ट

42
यह स्वीकृत उत्तर होना चाहिए। आधिकारिक दस्तावेज के लिए indirectकीवर्ड तर्क बेशक विरल और अमित्र है, जो संभवत: इस आवश्यक तकनीक का अंधकार के लिए खातों। मैंने इस फीचर के लिए कई अवसरों पर py.test साइट को स्कैन किया है - केवल खाली, पुराने और आने के लिए। कड़वाहट एक ऐसी जगह है जिसे निरंतर एकीकरण के रूप में जाना जाता है। Stackoverflow के लिए धन्यवाद ओडिन।
सेसिल करी

1
ध्यान दें कि इस पद्धति में पैरामीटर को शामिल करने के लिए आपके परीक्षणों का नाम बदल जाता है, जो वांछित नहीं हो सकता है या नहीं। test_tc1बन जाता है test_tc1[tester0]
जज

1
तो indirect=Trueसभी फिक्स्चर, सही करने के लिए मापदंडों पर हाथ? क्योंकि दस्तावेज़ीकरण में स्पष्ट रूप से अप्रत्यक्ष xindirect=['x']
पैरामीरीज़ेशन के

11

आप रिक्वेस्ट फ़ंक्शन (और इस तरह अपने टेस्टर क्लास से) रिक्वेस्टिंग मॉड्यूल / क्लास / फंक्शन को एक्सेस कर सकते हैं, एक फिक्सेटर फंक्शन से टेस्ट रिक्वेस्ट रिक्वेस्ट करने के साथ इंटरैक्ट कर सकते हैं । तो आप एक वर्ग या मॉड्यूल पर कुछ मापदंडों की घोषणा कर सकते हैं और परीक्षक स्थिरता इसे उठा सकते हैं।


3
मुझे पता है कि मैं ऐसा कुछ कर सकता हूं: (डॉक्स से) @ pytest.fixture (गुंजाइश = "मॉड्यूल", params = ["merlinux.eu", "mail.python.org"]]) लेकिन मुझे इसे करने की आवश्यकता है परीक्षण मॉड्यूल। मैं गतिशील रूप से जुड़नार के लिए परम कैसे जोड़ सकता हूं?
मैगी 17

2
बात यह है कि एक फिक्सेटर फ़ंक्शन से परीक्षण संदर्भ का अनुरोध करने के लिए बातचीत नहीं करनी है, लेकिन एक फ़िक्शन फ़ंक्शन के लिए तर्कों को पारित करने के लिए एक अच्छी तरह से परिभाषित तरीका है। फ़िक्चर फ़ंक्शन को परीक्षण संदर्भ के अनुरोध के एक प्रकार के बारे में पता होना चाहिए, केवल नामों पर सहमति के साथ तर्क प्राप्त करने में सक्षम होने के लिए। उदाहरण के लिए एक लिखने में सक्षम होना चाहते हैं @fixture def my_fixture(request)और उसके बाद @pass_args(arg1=..., arg2=...) def test(my_fixture)और इन आर्ग प्राप्त my_fixture()इस तरह arg1 = request.arg1, arg2 = request.arg2। क्या अब py.test में ऐसा कुछ संभव है?
पिओटर डोब्रोगोस्ट

7

मुझे कोई दस्तावेज़ नहीं मिला, हालांकि, यह नवीनतम संस्करण में काम करने लगता है।

@pytest.fixture
def tester(tester_arg):
    """Create tester object"""
    return MyTester(tester_arg)


class TestIt:
    @pytest.mark.parametrize('tester_arg', [['var1', 'var2']])
    def test_tc1(self, tester):
       tester.dothis()
       assert 1

इसे इंगित करने के लिए धन्यवाद - यह सभी का सबसे साफ समाधान जैसा लगता है। मुझे नहीं लगता कि यह पहले के संस्करणों में संभव हुआ करता था, लेकिन यह स्पष्ट है कि यह अब है। क्या आप जानते हैं कि आधिकारिक डॉक्स में कहीं भी इस फॉर्म का उल्लेख है ? मुझे यह पसंद नहीं आया, लेकिन यह स्पष्ट रूप से काम करता है। मैंने इस उदाहरण को शामिल करने के लिए अपना जवाब अपडेट कर दिया है, धन्यवाद।
इगुआनाटूट

1
मुझे लगता है कि यह फीचर में संभव नहीं होगा, अगर आप github.com/pytest-dev/pytest/issues/5712 और संबंधित (मर्ज किए गए) PR पर एक नज़र डालें ।
Nadège


1
स्पष्ट करने के लिए, @ Maspe36 यह संकेत दे रहा है कि पीआर द्वारा लिंक किया Nadègeगया था। इस प्रकार, यह अनिर्दिष्ट सुविधा (मुझे लगता है कि यह अभी भी अनिर्दिष्ट है?) अभी भी जीवित है।
ब्लेथेयर

6

थोड़ा सा इमिरिक के उत्तर को बेहतर बनाने के लिए : इस समस्या को हल करने का एक और सुरुचिपूर्ण तरीका "पैरामीटर जुड़नार" बनाना है। मैं व्यक्तिगत रूप से अधिक इसे पसंद करते indirectकी सुविधा pytest। यह सुविधा उपलब्ध है pytest_cases, और मूल विचार Sup3rGeo द्वारा सुझाया गया था ।

import pytest
from pytest_cases import param_fixture

# create a single parameter fixture
var = param_fixture("var", [['var1', 'var2']], ids=str)

@pytest.fixture
def tester(var):
    """Create tester object"""
    return MyTester(var)

class TestIt:
    def test_tc1(self, tester):
       tester.dothis()
       assert 1

ध्यान दें कि pytest-casesयह भी प्रदान करता है @pytest_fixture_plusकि आप अपने जुड़नार पर पैरामीट्राइजेशन के निशान का उपयोग कर सकते हैं, और @cases_dataयह आपको एक अलग मॉड्यूल में कार्यों से अपने मापदंडों को स्रोत करने की अनुमति देता है। देखें दस्तावेज़ जानकारी के लिए। मैं लेखक हूँ वैसे;)


1
यह अब सादे पाइस्टेस्ट में भी काम करने लगता है (मेरे पास v5.3.1 है)। यही है, मैं बिना इस काम को पाने में सक्षम था param_fixtureइस जवाब को देखें । मुझे हालांकि डॉक्स में इसका कोई उदाहरण नहीं मिला; क्या आप इस बारे में कुछ भी जानते हैं?
इगुआनाउत

जानकारी और लिंक के लिए धन्यवाद! मुझे नहीं पता था कि यह संभव था। आइए एक आधिकारिक दस्तावेज की प्रतीक्षा करें कि यह देखने के लिए कि उनके पास क्या है।
स्माइरी

2

मैंने एक मजाकिया डेकोरेटर बनाया, जो इस तरह के जुड़नार लिखने की अनुमति देता है:

@fixture_taking_arguments
def dog(request, /, name, age=69):
    return f"{name} the dog aged {age}"

यहां, /आपके बाईं ओर अन्य जुड़नार हैं, और दाईं ओर आपके पास ऐसे पैरामीटर हैं जिनका उपयोग करके आपूर्ति की जाती है:

@dog.arguments("Buddy", age=7)
def test_with_dog(dog):
    assert dog == "Buddy the dog aged 7"

यह उसी तरह से कार्य करता है जैसे कार्य तर्क कार्य करते हैं। यदि आप ageतर्क की आपूर्ति नहीं करते हैं, तो 69इसके बजाय डिफ़ॉल्ट एक का उपयोग किया जाता है। यदि आप आपूर्ति नहीं करते हैं name, या dog.argumentsडेकोरेटर को छोड़ देते हैं , तो आपको नियमित मिलता है TypeError: dog() missing 1 required positional argument: 'name'। यदि आपके पास एक और स्थिरता है जो तर्क लेती है name, तो यह इस के साथ संघर्ष नहीं करता है।

Async जुड़नार भी समर्थित हैं।

इसके अतिरिक्त, यह आपको एक अच्छा सेटअप योजना देता है:

$ pytest test_dogs_and_owners.py --setup-plan

SETUP    F dog['Buddy', age=7]
...
SETUP    F dog['Champion']
SETUP    F owner (fixtures used: dog)['John Travolta']

एक पूर्ण उदाहरण:

from plugin import fixture_taking_arguments

@fixture_taking_arguments
def dog(request, /, name, age=69):
    return f"{name} the dog aged {age}"


@fixture_taking_arguments
def owner(request, dog, /, name="John Doe"):
    yield f"{name}, owner of {dog}"


@dog.arguments("Buddy", age=7)
def test_with_dog(dog):
    assert dog == "Buddy the dog aged 7"


@dog.arguments("Champion")
class TestChampion:
    def test_with_dog(self, dog):
        assert dog == "Champion the dog aged 69"

    def test_with_default_owner(self, owner, dog):
        assert owner == "John Doe, owner of Champion the dog aged 69"
        assert dog == "Champion the dog aged 69"

    @owner.arguments("John Travolta")
    def test_with_named_owner(self, owner):
        assert owner == "John Travolta, owner of Champion the dog aged 69"

डेकोरेटर के लिए कोड:

import pytest
from dataclasses import dataclass
from functools import wraps
from inspect import signature, Parameter, isgeneratorfunction, iscoroutinefunction, isasyncgenfunction
from itertools import zip_longest, chain


_NOTHING = object()


def _omittable_parentheses_decorator(decorator):
    @wraps(decorator)
    def wrapper(*args, **kwargs):
        if not kwargs and len(args) == 1 and callable(args[0]):
            return decorator()(args[0])
        else:
            return decorator(*args, **kwargs)
    return wrapper


@dataclass
class _ArgsKwargs:
    args: ...
    kwargs: ...

    def __repr__(self):
        return ", ".join(chain(
               (repr(v) for v in self.args), 
               (f"{k}={v!r}" for k, v in self.kwargs.items())))


def _flatten_arguments(sig, args, kwargs):
    assert len(sig.parameters) == len(args) + len(kwargs)
    for name, arg in zip_longest(sig.parameters, args, fillvalue=_NOTHING):
        yield arg if arg is not _NOTHING else kwargs[name]


def _get_actual_args_kwargs(sig, args, kwargs):
    request = kwargs["request"]
    try:
        request_args, request_kwargs = request.param.args, request.param.kwargs
    except AttributeError:
        request_args, request_kwargs = (), {}
    return tuple(_flatten_arguments(sig, args, kwargs)) + request_args, request_kwargs


@_omittable_parentheses_decorator
def fixture_taking_arguments(*pytest_fixture_args, **pytest_fixture_kwargs):
    def decorator(func):
        original_signature = signature(func)

        def new_parameters():
            for param in original_signature.parameters.values():
                if param.kind == Parameter.POSITIONAL_ONLY:
                    yield param.replace(kind=Parameter.POSITIONAL_OR_KEYWORD)

        new_signature = original_signature.replace(parameters=list(new_parameters()))

        if "request" not in new_signature.parameters:
            raise AttributeError("Target function must have positional-only argument `request`")

        is_async_generator = isasyncgenfunction(func)
        is_async = is_async_generator or iscoroutinefunction(func)
        is_generator = isgeneratorfunction(func)

        if is_async:
            @wraps(func)
            async def wrapper(*args, **kwargs):
                args, kwargs = _get_actual_args_kwargs(new_signature, args, kwargs)
                if is_async_generator:
                    async for result in func(*args, **kwargs):
                        yield result
                else:
                    yield await func(*args, **kwargs)
        else:
            @wraps(func)
            def wrapper(*args, **kwargs):
                args, kwargs = _get_actual_args_kwargs(new_signature, args, kwargs)
                if is_generator:
                    yield from func(*args, **kwargs)
                else:
                    yield func(*args, **kwargs)

        wrapper.__signature__ = new_signature
        fixture = pytest.fixture(*pytest_fixture_args, **pytest_fixture_kwargs)(wrapper)
        fixture_name = pytest_fixture_kwargs.get("name", fixture.__name__)

        def parametrizer(*args, **kwargs):
            return pytest.mark.parametrize(fixture_name, [_ArgsKwargs(args, kwargs)], indirect=True)

        fixture.arguments = parametrizer

        return fixture
    return decorator
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.