Передайте параметр функції кріплення


114

Я використовую py.test для тестування деякого коду DLL, загорнутого в клас MyTester python. Для перевірки мені потрібно записати деякі тестові дані під час тестів і зробити більшу обробку після цього. Оскільки у мене є багато тестових _... файлів, я хочу використовувати повторно створення об'єкта тестеру (екземпляр MyTester) для більшості моїх тестів.

Оскільки об’єктом тестера є той, який отримав посилання на змінні та функції DLL, мені потрібно передати список змінних DLL об'єкту тестера для кожного тестового файлу (змінні, які потрібно зареєструвати, однакові для тесту_. . файл). Зміст списку використовується для реєстрації зазначених даних.

Моя ідея - зробити це якось так:

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"])

Але мені потрібно параметризація безпосередньо в тестовому модулі. Чи можна отримати доступ до атрибута 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_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
Я спробував використовувати це рішення, але у мене виникли проблеми з проходженням декількох параметрів або використанням імен змінних, крім запиту. Я в кінцевому підсумку використовував рішення @Iguananaut.
Віктор Уріарте

42
Це має бути прийнятою відповіддю. Офіційна документація по indirectключовому слову аргументу свідомо мізерна і недружелюбно, що , ймовірно , пояснює невідомість цієї важливої техніки. Я неодноразово обшукував сайт py.test за цією особливістю - лише щоб вийшов порожнім, старішим і ошелешеним. Гіркота - це місце, відоме як безперервна інтеграція. Дякую Одіну за Stackoverflow.
Сесіль Карі

1
Зауважте, що цей метод змінює назву ваших тестів, включаючи параметр, який може бути, а може і не бажати. test_tc1стає test_tc1[tester0].
jjj

1
Тож indirect=Trueпередайте параметри всім називаним світильникам, правда? Оскільки в документації прямо вказані світильники для непрямої параметризації, наприклад для світильника під назвою x:indirect=['x']
winklerrr

11

Ви можете отримати доступ до запитувального модуля / класу / функції через функції кріплення (і, отже, з вашого класу Тестер), див. Взаємодію із запитом тестового контексту від функції кріплення . Таким чином, ви можете оголосити деякі параметри класу або модуля, і тестовий пристрій може забрати його.


3
Я знаю, що я можу зробити щось подібне: (з документів) @ pytest.fixture (obseg = "модуль", params = ["merlinux.eu", "mail.python.org"]) Але мені потрібно зробити це в модуль тестування. Як я можу динамічно додавати параметри до світильників?
Меггі

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.

@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/isissue/5712 та пов'язаний з ним (об'єднаний) PR.
Nadège

Це було повернено github.com/pytest-dev/pytest/pull/6914
Maspe36

1
Для уточнення, @ Maspe36 вказує на те, що зв'язок, пов'язаний з PR, Nadègeбув повернений. Таким чином, ця незадокументована особливість (я думаю, що вона все ще незадокументована?) Все ще живе.
blthayer

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. Дивіться цю відповідь . Я не міг знайти жодного подібного прикладу в документах; ти щось знаєш про це?
Ігуананавт

дякую за інформацію та посилання! Я не здогадувався, що це можливо. Зачекаємо офіційної документації, щоб побачити, що вони мають на увазі.
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'. Якщо у вас є інше кріплення, яке бере аргументи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 та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.