Przekaż parametr do funkcji urządzenia

114

Używam py.test do testowania kodu DLL opakowanego w MyTester klasy Pythona. W celu walidacji muszę rejestrować niektóre dane testowe podczas testów, a następnie wykonywać dalsze przetwarzanie. Ponieważ mam wiele plików testowych _..., chcę ponownie użyć tworzenia obiektu testera (instancji MyTester) dla większości moich testów.

Ponieważ obiekt tester jest tym, który ma odwołania do zmiennych i funkcji biblioteki DLL, muszę przekazać listę zmiennych biblioteki DLL do obiektu testera dla każdego z plików testowych (zmienne, które mają być rejestrowane, są takie same dla pliku test_ .. plik). Treść listy służy do logowania określonych danych.

Mój pomysł jest taki, aby zrobić to w ten sposób:

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

Czy można to osiągnąć w ten sposób, czy jest jeszcze bardziej elegancki sposób?

Zwykle mógłbym to zrobić dla każdej metody testowej za pomocą jakiejś funkcji konfiguracji (w stylu xUnit). Ale chcę uzyskać możliwość ponownego wykorzystania. Czy ktoś wie, czy jest to w ogóle możliwe z oprawami?

Wiem, że mogę zrobić coś takiego: (z dokumentów)

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

Ale potrzebuję parametryzacji bezpośrednio w module testowym. Czy jest możliwy dostęp do atrybutu params urządzenia z modułu testowego?

maggie
źródło

Odpowiedzi:

101

Aktualizacja: Ponieważ jest to zaakceptowana odpowiedź na to pytanie i czasami jest przegłosowana, powinienem dodać aktualizację. Mimo, że mój oryginalny odpowiedź (poniżej) było jedynym sposobem, aby to zrobić w starszych wersjach pytest jak inni nie zauważyć pytest teraz obsługuje pośrednie parametryzacji urządzeń. Na przykład możesz zrobić coś takiego (przez @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

Jednak, chociaż ta forma parametryzacji pośredniej jest wyraźna, jak wskazuje @Yukihiko Shinoda , obsługuje ona teraz formę niejawnej parametryzacji pośredniej (chociaż nie mogłem znaleźć żadnego oczywistego odniesienia do tego w oficjalnych dokumentach):

# 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

Nie wiem dokładnie, jaka jest semantyka tego formularza, ale wydaje się, że pytest.mark.parametrizerozpoznaje, że chociaż test_tc1metoda nie przyjmuje argumentu o nazwie tester_arg, testerurządzenie, którego używa, robi, więc przekazuje sparametryzowany argument dalej przez testerurządzenie.


Miałem podobny problem - mam wywołane urządzenie test_package, a później chciałem móc przekazać opcjonalny argument do tego urządzenia podczas uruchamiania go w określonych testach. Na przykład:

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

(Dla tych celów nie ma znaczenia, co robi urządzenie lub jaki typ zwracanego obiektu package).

Byłoby zatem pożądane, aby w jakiś sposób użyć tego urządzenia w funkcji testowej w taki sposób, żebym mógł również określić versionargument dla tego urządzenia do użycia w tym teście. Obecnie nie jest to możliwe, ale może być fajną funkcją.

W międzyczasie łatwo było sprawić, że moje urządzenie po prostu zwróciło funkcję, która wykonuje całą pracę, jaką wykonało wcześniej urządzenie, ale pozwala mi określić versionargument:

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

    return make_test_package

Teraz mogę użyć tego w mojej funkcji testowej, takiej jak:

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

i tak dalej.

Próba rozwiązania OP zmierzała we właściwym kierunku i, jak sugeruje odpowiedź @ hpk42 , MyTester.__init__można po prostu zapisać odniesienie do żądania, takie jak:

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"

Następnie użyj tego, aby zaimplementować urządzenie, takie jak:

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

W razie potrzeby MyTesterklasę można nieco przebudować, aby jej .argsatrybut mógł zostać zaktualizowany po jej utworzeniu, aby dostosować zachowanie dla poszczególnych testów.

Iguananaut
źródło
Dzięki za podpowiedź z funkcją wewnątrz oprawy. Minęło trochę czasu, zanim mogłem ponownie nad tym popracować, ale jest to całkiem przydatne!
maggie
2
Miły krótki post na ten temat: alysivji.github.io/pytest-fixures-with-function-arguments.html
maggie
czy nie otrzymujesz błędu mówiącego: „Urządzenia nie mają być wywoływane bezpośrednio, ale są tworzone automatycznie, gdy funkcje testowe żądają ich jako parametrów.”?
nz_21
153

W rzeczywistości jest to obsługiwane natywnie w py.test przez pośrednią parametryzację .

W twoim przypadku miałbyś:

@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
imiryczny
źródło
Ach, to całkiem fajne (myślę, że twój przykład może być jednak trochę przestarzały - różni się od przykładów w oficjalnych dokumentach). Czy to stosunkowo nowa funkcja? Nigdy wcześniej go nie spotkałem. To również dobre rozwiązanie problemu - w pewnym sensie lepsze niż moja odpowiedź.
Iguananaut
2
Próbowałem użyć tego rozwiązania, ale miałem problemy z przekazywaniem wielu parametrów lub używaniem nazw zmiennych innych niż żądanie. Skończyło się na rozwiązaniu @Iguananaut.
Victor Uriarte
42
To powinna być akceptowana odpowiedź. Oficjalna dokumentacja dla indirectargumentu słowa kluczowego jest wprawdzie rzadki i nieprzyjazny, który prawdopodobnie odpowiada za zapomnienie o tej podstawowej techniki. Wielokrotnie przeszukiwałem witrynę py.test w poszukiwaniu tej właśnie funkcji - tylko po to, by wydać się pusta, starsza i oszołomiona. Goryczka to miejsce znane jako ciągła integracja. Podziękuj Odinowi za Stackoverflow.
Cecil Curry,
1
Zwróć uwagę, że ta metoda zmienia nazwę testów, aby uwzględnić parametr, który może być pożądany lub nie. test_tc1staje się test_tc1[tester0].
jjj
1
Więc indirect=Trueprzekazuje parametry wszystkim wywołanym urządzeniom, prawda? Ponieważ dokumentacja wyraźnie wymienia urządzenia do pośredniej parametryzacji, np. Dla urządzenia o nazwie x:indirect=['x']
winklerrr
11

Możesz uzyskać dostęp do żądającego modułu / klasy / funkcji z funkcji urządzenia (a tym samym z klasy Tester), zobacz interakcję z żądaniem kontekstu testowego z funkcji urządzenia . Możesz więc zadeklarować niektóre parametry w klasie lub module, a urządzenie testujące może je odebrać.

hpk42
źródło
3
Wiem, że mogę zrobić coś takiego: (z dokumentacji) @ pytest.fixture (scope = "module", params = ["merlinux.eu", "mail.python.org"]) Ale muszę to zrobić w moduł testowy. Jak mogę dynamicznie dodawać parametry do urządzeń?
maggie,
2
Nie chodzi o to, aby musieć wchodzić w interakcje z żądaniem kontekstu testowego z funkcji urządzenia, ale mieć dobrze zdefiniowany sposób przekazywania argumentów do funkcji urządzenia. Funkcja fixture nie powinna być świadoma typu żądania kontekstu testowego tylko po to, aby móc odbierać argumenty o uzgodnionych nazwach. Na przykład chciałoby się móc napisać, @fixture def my_fixture(request)a następnie @pass_args(arg1=..., arg2=...) def test(my_fixture)uzyskać te argumenty w my_fixture()ten sposób arg1 = request.arg1, arg2 = request.arg2. Czy coś takiego jest teraz możliwe w py.test?
Piotr Dobrogost
7

Nie mogłem znaleźć żadnego dokumentu, jednak wygląda na to, że działa w najnowszej wersji 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
Yukihiko Shinoda
źródło
Dziękuję za zwrócenie uwagi - to wydaje się być najczystszym rozwiązaniem ze wszystkich. Nie sądzę, aby było to możliwe we wcześniejszych wersjach, ale jest jasne, że teraz jest. Czy wiesz, czy ten formularz jest wymieniony gdziekolwiek w oficjalnych dokumentach ? Nie mogłem znaleźć czegoś podobnego, ale najwyraźniej działa. Zaktualizowałem moją odpowiedź, aby zawierała ten przykład, dzięki.
Iguananaut
1
Myślę, że nie będzie to możliwe w tej funkcji, jeśli spojrzysz na github.com/pytest-dev/pytest/issues/5712 i powiązany (połączony) PR.
Nadège
1
Aby wyjaśnić, @ Maspe36 wskazuje, że PR powiązany przez Nadègezostał cofnięty. Tak więc ta nieudokumentowana funkcja (myślę, że nadal nie jest udokumentowana?) Nadal istnieje.
blthayer
6

Aby nieco poprawić odpowiedź imirica : innym eleganckim sposobem rozwiązania tego problemu jest utworzenie „ ustawiania parametrów”. Osobiście wolę to od indirectfunkcji pytest. Ta funkcja jest dostępna od pytest_cases, a oryginalny pomysł zasugerował 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

Zauważ, że pytest-caseszapewnia również @pytest_fixture_plusmożliwość używania znaczników parametryzacji w urządzeniach, a także @cases_dataumożliwia pobieranie parametrów z funkcji w oddzielnym module. Szczegółowe informacje można znaleźć w dokumencie . Swoją drogą jestem autorem;)

smarie
źródło
1
Wydaje się, że teraz działa to również w prostym pytest (mam wersję 5.3.1). Oznacza to, że udało mi się to bez param_fixture. Zobacz tę odpowiedź . Nie mogłem jednak znaleźć żadnego podobnego przykładu w dokumentach; Wiesz cokolwiek o tym?
Iguananaut
dzięki za informację i link! Nie miałem pojęcia, że ​​to wykonalne. Poczekajmy na oficjalną dokumentację, aby zobaczyć, co mają na myśli.
smarie
2

Zrobiłem zabawny dekorator, który pozwala pisać takie urządzenia:

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

Tutaj, po lewej stronie /masz inne urządzenia, a po prawej masz parametry, które są dostarczane za pomocą:

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

Działa to w ten sam sposób, w jaki działają argumenty funkcji. Jeśli nie podasz ageargumentu 69, zostanie użyty domyślny . jeśli nie dostarczysz namelub pominiesz dog.argumentsdekorator, otrzymasz zwykły TypeError: dog() missing 1 required positional argument: 'name'. Jeśli masz inne urządzenie, które przyjmuje argument name, nie powoduje to konfliktu z tym.

Obsługiwane są również urządzenia asynchroniczne.

Dodatkowo daje to ładny plan konfiguracji:

$ 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']

Pełny przykład:

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"

Kod dekoratora:

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
wiewiórka
źródło