Übergeben Sie einen Parameter an eine Fixture-Funktion

113

Ich verwende py.test, um einen DLL-Code zu testen, der in eine Python-Klasse MyTester eingeschlossen ist. Zur Validierung muss ich einige Testdaten während der Tests protokollieren und anschließend weitere Verarbeitungen durchführen. Da ich viele Testdateien habe, möchte ich die Testerobjekterstellung (Instanz von MyTester) für die meisten meiner Tests wiederverwenden.

Da das Testerobjekt die Verweise auf die Variablen und Funktionen der DLL enthält, muss ich für jede der Testdateien eine Liste der Variablen der DLL an das Testerobjekt übergeben (die zu protokollierenden Variablen sind für einen Test identisch. . Datei). Der Inhalt der Liste wird zur Protokollierung der angegebenen Daten verwendet.

Meine Idee ist es, es irgendwie so zu machen:

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

Ist es möglich, dies zu erreichen, oder gibt es sogar einen eleganteren Weg?

Normalerweise könnte ich es für jede Testmethode mit einer Art Setup-Funktion (xUnit-Stil) tun. Aber ich möchte eine Art Wiederverwendung erlangen. Weiß jemand, ob dies mit Scheinwerfern überhaupt möglich ist?

Ich weiß, dass ich so etwas tun kann: (aus den Dokumenten)

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

Ich muss aber die Parametrisierung direkt im Testmodul vornehmen. Ist es möglich, über das Testmodul auf das Parameter params des Geräts zuzugreifen?

Maggie
quelle

Antworten:

100

Update: Da dies die akzeptierte Antwort auf diese Frage ist und manchmal immer noch positiv bewertet wird, sollte ich ein Update hinzufügen. Obwohl meine ursprüngliche Antwort (unten) die einzige Möglichkeit war, dies in älteren Versionen von pytest zu tun, wie andere festgestellt haben , unterstützt pytest jetzt die indirekte Parametrisierung von Vorrichtungen. Zum Beispiel können Sie so etwas tun (via @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

Obwohl diese Form der indirekten Parametrisierung explizit ist, unterstützt @Yukihiko Shinoda, wie sie betont , jetzt eine Form der impliziten indirekten Parametrisierung (obwohl ich in den offiziellen Dokumenten keinen offensichtlichen Hinweis darauf finden konnte):

# 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

Ich weiß nicht genau, wie die Semantik dieses Formulars aussieht, aber es scheint zu pytest.mark.parametrizeerkennen, dass die test_tc1Methode zwar kein Argument mit dem Namen verwendet tester_arg, das verwendete testerGerät dies jedoch tut, sodass das parametrisierte Argument durch das testerGerät weitergeleitet wird.


Ich hatte ein ähnliches Problem - ich habe ein Fixture namens test_packageund wollte später in der Lage sein, diesem Fixture ein optionales Argument zu übergeben, wenn es in bestimmten Tests ausgeführt wird. Beispielsweise:

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

(Für diese Zwecke spielt es keine Rolle, was das Gerät tut oder welche Art von Objekt das zurückgegebene ist package).

Es wäre dann wünschenswert, dieses Gerät irgendwie in einer Testfunktion so zu verwenden, dass ich auch das versionArgument für dieses Gerät angeben kann, das mit diesem Test verwendet werden soll. Dies ist derzeit nicht möglich, könnte aber eine nette Funktion sein.

In der Zwischenzeit war es einfach genug, mein Gerät dazu zu bringen, einfach eine Funktion zurückzugeben , die die gesamte Arbeit des Geräts erledigt, aber mir erlaubt, das folgende versionArgument anzugeben :

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

    return make_test_package

Jetzt kann ich dies in meiner Testfunktion verwenden wie:

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

und so weiter.

Der Lösungsversuch des OP ging in die richtige Richtung, und wie aus der Antwort von @ hpk42 hervorgeht , MyTester.__init__könnte der Verweis auf die Anfrage einfach wie folgt gespeichert werden:

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"

Verwenden Sie dies dann, um das Gerät wie folgt zu implementieren:

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

Falls gewünscht, kann die MyTesterKlasse ein wenig umstrukturiert werden, damit ihr .argsAttribut nach dem Erstellen aktualisiert werden kann, um das Verhalten für einzelne Tests zu optimieren.

Leguananaut
quelle
Vielen Dank für den Hinweis mit der Funktion im Gerät. Es hat einige Zeit gedauert, bis ich wieder daran arbeiten konnte, aber das ist ziemlich nützlich!
Maggie
2
Ein schöner kurzer Beitrag zu diesem Thema: alysivji.github.io/pytest-fixures-with-function-arguments.html
Maggie
Erhalten Sie keine Fehlermeldung: "Fixtures sollen nicht direkt aufgerufen werden, sondern werden automatisch erstellt, wenn Testfunktionen sie als Parameter anfordern."?
nz_21
152

Dies wird in py.test über die indirekte Parametrisierung nativ unterstützt .

In Ihrem Fall hätten Sie:

@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
imirisch
quelle
Ah, das ist ziemlich nett (ich denke, Ihr Beispiel ist vielleicht etwas veraltet - es unterscheidet sich von den Beispielen in den offiziellen Dokumenten). Ist das eine relativ neue Funktion? Ich habe es noch nie erlebt. Dies ist auch eine gute Lösung für das Problem - in gewisser Hinsicht besser als meine Antwort.
Iguananaut
2
Ich habe versucht, diese Lösung zu verwenden, hatte jedoch Probleme, mehrere Parameter zu übergeben oder andere Variablennamen als die Anforderung zu verwenden. Am Ende habe ich die Lösung von @Iguananaut verwendet.
Victor Uriarte
42
Dies sollte die akzeptierte Antwort sein. Die offizielle Dokumentation für das indirectSchlüsselwortargument ist zugegebenermaßen spärlich und unfreundlich, was wahrscheinlich die Unklarheit dieser wesentlichen Technik erklärt. Ich habe die py.test-Site mehrmals nach genau dieser Funktion durchsucht - nur um leer, älter und verwirrt zu erscheinen. Bitterkeit ist ein Ort, der als kontinuierliche Integration bekannt ist. Danke Odin für Stackoverflow.
Cecil Curry
1
Beachten Sie, dass diese Methode den Namen Ihrer Tests so ändert, dass sie den Parameter enthält, der möglicherweise erwünscht ist oder nicht. test_tc1wird test_tc1[tester0].
JJJ
1
Übergibt also indirect=TrueParameter an alle genannten Geräte, oder? Weil die Dokumentation die Geräte für die indirekte Parametrisierung explizit benennt, z. B. für ein Gerät mit dem Namen x:indirect=['x']
winklerrr
11

Sie können über Fixture-Funktionen (und damit über Ihre Tester-Klasse) auf das anfordernde Modul / die anfordernde Funktion / Funktion zugreifen. Weitere Informationen finden Sie unter Interaktion mit dem anfordernden Testkontext über eine Fixture-Funktion . Sie können also einige Parameter für eine Klasse oder ein Modul deklarieren und das Testgerät kann sie abrufen.

hpk42
quelle
3
Ich weiß, dass ich so etwas tun kann: (aus den Dokumenten) @ pytest.fixture (scope = "module", params = ["merlinux.eu", "mail.python.org"]) Aber ich muss es tun das Testmodul. Wie kann ich den Fixtures dynamisch Parameter hinzufügen?
Maggie
2
Es geht nicht darum, mit der Anforderung eines Testkontexts von einer Fixture-Funktion interagieren zu müssen, sondern eine genau definierte Möglichkeit zu haben, Argumente an eine Fixture-Funktion zu übergeben. Die Fixture-Funktion sollte sich keiner Art von anforderndem Testkontext bewusst sein, um Argumente mit vereinbarten Namen empfangen zu können. Zum Beispiel würde man in der Lage sein möchten , schreiben @fixture def my_fixture(request)und dann @pass_args(arg1=..., arg2=...) def test(my_fixture)und diese args in bekommen my_fixture()wie dieses arg1 = request.arg1, arg2 = request.arg2. Ist so etwas jetzt in py.test möglich?
Piotr Dobrogost
7

Ich konnte kein Dokument finden, es scheint jedoch in der neuesten Version von pytest zu funktionieren.

@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
quelle
Vielen Dank für den Hinweis - dies scheint die sauberste Lösung von allen zu sein. Ich denke nicht, dass dies in früheren Versionen möglich war, aber es ist klar, dass es jetzt möglich ist. Wissen Sie, ob dieses Formular irgendwo in den offiziellen Dokumenten erwähnt wird ? Ich konnte so etwas nicht finden, aber es funktioniert eindeutig. Ich habe meine Antwort aktualisiert , um dieses Beispiel aufzunehmen, danke.
Iguananaut
1
Ich denke, es wird in der Funktion nicht möglich sein, wenn Sie sich github.com/pytest-dev/pytest/issues/5712 und die zugehörige (zusammengeführte) PR ansehen .
Nadège
Dies wurde zurückgesetzt github.com/pytest-dev/pytest/pull/6914
Maspe36
1
Zur Verdeutlichung gibt @ Maspe36 an, dass die durch verknüpfte PR Nadègezurückgesetzt wurde. Somit lebt diese undokumentierte Funktion (ich denke, sie ist immer noch undokumentiert?) Noch.
Blthayer
6

Um die Antwort von imiric ein wenig zu verbessern : Eine andere elegante Möglichkeit, dieses Problem zu lösen, besteht darin, "Parameter-Fixtures" zu erstellen. Ich persönlich bevorzuge es gegenüber der indirectFunktion von pytest. Diese Funktion ist bei erhältlich pytest_casesund die ursprüngliche Idee wurde von Sup3rGeo vorgeschlagen .

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

Beachten Sie, dass pytest-casesauch bietet @pytest_fixture_plus, mit denen Sie Parametrisierung Markierungen auf Fixtures verwenden, und @cases_data, mit denen Sie Ihre Parameter von Funktionen in einem separaten Modul beziehen. Siehe Dokument für Details. Ich bin übrigens der Autor;)

smarie
quelle
1
Dies scheint jetzt auch im einfachen Pytest zu funktionieren (ich habe v5.3.1). Das heißt, ich konnte dies ohne zum Laufen bringen param_fixture. Siehe diese Antwort . Ich konnte jedoch kein solches Beispiel in den Dokumenten finden. Weißt du etwas darüber?
Iguananaut
danke für die info und den link! Ich hatte keine Ahnung, dass dies machbar war. Warten wir auf eine offizielle Dokumentation, um zu sehen, was sie vorhaben.
Smarie
1

Ich habe einen lustigen Dekorateur gemacht, mit dem man solche Geräte schreiben kann:

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

Hier /haben Sie links von Ihnen andere Geräte und rechts haben Sie Parameter, die geliefert werden mit:

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

Dies funktioniert genauso wie Funktionsargumente. Wenn Sie das ageArgument nicht angeben 69, wird stattdessen das Standardargument verwendet. Wenn Sie nameden dog.argumentsDekorateur nicht liefern oder weglassen , erhalten Sie den regulären TypeError: dog() missing 1 required positional argument: 'name'. Wenn Sie ein anderes Gerät haben, das Argumente benötigt name, steht es nicht in Konflikt mit diesem.

Asynchrone Geräte werden ebenfalls unterstützt.

Darüber hinaus erhalten Sie einen schönen Einrichtungsplan:

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

Ein vollständiges Beispiel:

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"

Der Code für den Dekorateur:

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
Eichhörnchen
quelle