조명기 함수에 매개 변수 전달


114

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 속성에 액세스 할 수 있습니까?

답변:


101

업데이트 : 이 질문에 대한 답변이 허용되고 가끔씩 찬성 투표를 받기 때문에 업데이트 를 추가해야합니다. 내 원래 답변 (아래)은 다른 사람들 이 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속성이 생성 된 후 업데이트 될 수 있도록 개별 테스트의 동작을 조정할 수 있습니다.


조명기 내부의 기능에 대한 힌트에 감사드립니다. 다시 작업 할 수있을 때까지 시간이 좀 걸렸지 만 이것은 꽤 유용합니다!
maggie

2
이 주제에 대한 멋진 짧은 게시물 : alysivji.github.io/pytest-fixures-with-function-arguments.html
maggie

"Fixture는 직접 호출 할 수 없지만 테스트 함수가 매개 변수로 요청할 때 자동으로 생성됩니다."라는 오류 메시지가 표시되지 않습니까?
nz_21 19

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

아, 이것은 꽤 좋습니다 (당신의 예제는 약간 구식이라고 생각합니다. 상대적으로 새로운 기능입니까? 전에는 본 적이 없습니다. 이것은 문제에 대한 좋은 해결책이기도합니다.
Iguananaut

2
이 솔루션을 사용해 보았지만 여러 매개 변수를 전달하거나 요청 이외의 변수 이름을 사용하는 데 문제가있었습니다. 결국 @Iguananaut의 솔루션을 사용하게되었습니다.
빅터 Uriarte

42
이것은 받아 들여진 대답이어야합니다. 공식 문서 에 대한 indirect키워드 인수는 아마이 핵심 기술의 무명을 차지하고있는, 일반적으로 인정 하듯이 스파 스 비우호적이다. 나는이 기능에 대해 여러 차례 py.test 사이트를 샅샅이 뒤져서 텅 비어 있고, 오래되고, 당황 스러웠다. 쓴맛은 지속적인 통합으로 알려진 곳입니다. Stackoverflow에 대해 Odin에게 감사드립니다.
Cecil Curry

1
이 메서드는 매개 변수를 포함하도록 테스트 이름을 변경합니다. test_tc1됩니다 test_tc1[tester0].
JJJ

1
그래서 indirect=True매개 변수를 호출 된 모든 조명기에 넘겨 주죠? 문서 는 간접 매개 변수화를위한 조명기의 이름을 명시 적으로 지정 하기 때문에 , 예를 들어 다음과 같은 조명기 이름 x:indirect=['x']
winklerrr

11

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


3
(문서에서) @ pytest.fixture (scope = "module", params = [ "merlinux.eu", "mail.python.org"]) 다음과 같이 할 수 있다는 것을 알고 있지만 테스트 모듈. 어떻게 픽스쳐에 매개 변수를 동적으로 추가 할 수 있습니까?
maggie

2
요점은 픽스처 함수에서 테스트 컨텍스트요청하는 것과 상호 작용할 필요가 없지만 픽스처 함수에 인수를 전달하는 잘 정의 된 방법을 갖는 것입니다. Fixture 함수는 합의 된 이름으로 인수를 수신 할 수 있도록 요청하는 테스트 컨텍스트 유형을 인식 할 필요가 없습니다. 예를 들어 @fixture def my_fixture(request)다음 @pass_args(arg1=..., arg2=...) def test(my_fixture)my_fixture()같이 이러한 인수 를 작성 하고 가져올 수 있기를 원합니다 arg1 = request.arg1, arg2 = request.arg2. 지금 py.test에서 이와 같은 것이 가능합니까?
Piotr Dobrogost

7

문서를 찾을 수 없지만 최신 버전의 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

이것을 지적 해 주셔서 감사합니다. 이것은 가장 깨끗한 해결책 인 것 같습니다. 나는 이것이 이전 버전에서 가능하다고 생각하지 않지만 지금은 분명합니다. 이 양식이 공식 문서 에 언급되어 있는지 알고 있습니까? 나는 그것과 비슷한 것을 찾을 수 없었지만 분명히 작동합니다. 이 예제를 포함하도록 답변 을 업데이트했습니다 . 감사합니다.
Iguananaut

1
github.com/pytest-dev/pytest/issues/5712 및 관련 (병합) PR을 살펴보면 기능에서 불가능할 것이라고 생각합니다 .
Nadège


1
명확히하기 위해 @ Maspe36은에 의해 연결된 PR Nadège이 되돌 렸음을 나타냅니다 . 따라서이 문서화되지 않은 기능 (아직 문서화되지 않은 것 같습니까?)은 여전히 ​​존재합니다.
blthayer

6

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별도의 모듈에서 함수에서 매개 변수를 소싱 할 수있다. 자세한 내용은 문서 를 참조하십시오. 그건 그렇고 나는 저자입니다;)


1
이것은 일반 pytest에서도 작동하는 것으로 보입니다 (v5.3.1이 있습니다). 즉, param_fixture. 이 답변을 참조하십시오 . 그래도 문서에서 이와 같은 예를 찾을 수 없습니다. 이것에 대해 아는 것이 있습니까?
Iguananaut

정보와 링크에 감사드립니다! 나는 이것이 가능한지 전혀 몰랐다. 그들이 염두에두고있는 것을 볼 수있는 공식 문서를 기다립니다.
smarie

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'. 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
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.