[python] 조명기 함수에 매개 변수 전달

py.test를 사용하여 Python 클래스 MyTester에 래핑 된 일부 DLL 코드를 테스트하고 있습니다. 검증을 위해 테스트 중에 일부 테스트 데이터를 기록하고 나중에 더 많은 처리를해야합니다. 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 스타일)을 사용하여 각 테스트 방법에 대해 수행 할 수 있습니다. 그러나 나는 어떤 종류의 재사용을 얻고 싶습니다. 이것이 조명기로 가능한지 아는 사람이 있습니까?

나는 다음과 같이 할 수 있다는 것을 안다. (문서에서)

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

하지만 테스트 모듈에서 직접 매개 변수화가 필요합니다.
테스트 모듈에서 조명기의 params 속성에 액세스 할 수 있습니까?



답변

업데이트 : 이 질문에 대한 답변이 허용되고 가끔씩 찬성 투표를 받기 때문에 업데이트 를 추가해야합니다. 내 원래 답변 (아래)은 다른 사람들 이 pytest가 이제 조명기의 간접 매개 변수화를 지원 한다고 지적했듯이 이전 버전의 pytest에서 이것을 수행하는 유일한 방법 이지만. 예를 들어 다음과 같이 할 수 있습니다 (@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_argtester가를 통해의 매개 변수화 인수를 전달하므로, 그것을 사용하고 있음을 고정이하는 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 ...

등등.

OP의 시도 된 솔루션은 올바른 방향으로 향했으며 @ 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속성이 생성 된 후 업데이트 될 수 있도록 개별 테스트의 동작을 조정할 수 있습니다.


답변

이것은 실제로 간접 매개 변수화 를 통해 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


답변

픽스처 함수 (따라서 Tester 클래스에서)에서 요청하는 모듈 / 클래스 / 함수에 액세스 할 수 있습니다 . 픽스처 함수에서 테스트 컨텍스트 요청과 상호 작용을 참조하십시오 . 따라서 클래스 또는 모듈에서 일부 매개 변수를 선언 할 수 있으며 테스터 픽스처가이를 선택할 수 있습니다.


답변

문서를 찾을 수 없지만 최신 버전의 pytest에서 작동하는 것 같습니다.

@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


답변

imiric의 대답을 조금 개선하려면 :이 문제를 해결하는 또 다른 우아한 방법은 “parameter fixtures”를 만드는 것입니다. 저는 개인적 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별도의 모듈에서 함수에서 매개 변수를 소싱 할 수있다. 자세한 내용은 문서 를 참조하십시오. 그건 그렇고 나는 저자입니다;)


답변

다음과 같은 고정물을 작성할 수있는 재미있는 데코레이터를 만들었습니다.

@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'. argument를 취하는 다른 조명기가 있다면 name이것과 충돌하지 않습니다.

비동기 픽스쳐도 지원됩니다.

또한 이것은 멋진 설정 계획을 제공합니다.

$ 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


답변