Passa un parametro a una funzione dispositivo


114

Sto usando py.test per testare del codice DLL racchiuso in una classe Python MyTester. A scopo di convalida, è necessario registrare alcuni dati di test durante i test e successivamente eseguire ulteriori elaborazioni. Dato che ho molti file _... di prova, desidero riutilizzare la creazione dell'oggetto tester (istanza di MyTester) per la maggior parte dei miei test.

Poiché l'oggetto tester è quello che ha ottenuto i riferimenti alle variabili e alle funzioni della DLL, ho bisogno di passare un elenco delle variabili della DLL all'oggetto tester per ciascuno dei file di test (le variabili da registrare sono le stesse per un test_ .. . file). Il contenuto dell'elenco deve essere utilizzato per registrare i dati specificati.

La mia idea è di farlo in qualche modo in questo modo:

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

È possibile ottenerlo in questo modo o esiste anche un modo più elegante?

Di solito potrei farlo per ogni metodo di prova con una sorta di funzione di configurazione (stile xUnit). Ma voglio ottenere una sorta di riutilizzo. Qualcuno sa se questo è possibile con i proiettori?

So di poter fare qualcosa del genere: (dai documenti)

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

Ma ho bisogno della parametrizzazione direttamente nel modulo di test. È possibile accedere all'attributo params della fixture dal modulo di test?

Risposte:


101

Aggiornamento: poiché questa è la risposta accettata a questa domanda e viene ancora votata a volte, dovrei aggiungere un aggiornamento. Sebbene la mia risposta originale (sotto) fosse l'unico modo per farlo nelle versioni precedenti di pytest, poiché altri hanno notato che pytest ora supporta la parametrizzazione indiretta dei dispositivi. Ad esempio puoi fare qualcosa del genere (tramite @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

Tuttavia, anche se questa forma di parametrizzazione indiretta è esplicito, come @Yukihiko Shinoda sottolinea che ora supporta una forma di implicita parametrizzazione indiretta (anche se non ho potuto trovare alcun riferimento ovvia a questo nella documentazione ufficiale):

# 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

Non so esattamente quali siano le semantiche di questo modulo, ma sembra che pytest.mark.parametrizericonosca che sebbene il test_tc1metodo non prenda un argomento denominato tester_arg, il testerdispositivo che sta utilizzando lo fa, quindi passa l'argomento parametrizzato attraverso il testerdispositivo.


Ho avuto un problema simile: ho chiamato un dispositivo test_packagee in seguito ho voluto essere in grado di passare un argomento opzionale a quel dispositivo durante l'esecuzione in test specifici. Per esempio:

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

(Non importa per questi scopi cosa fa il dispositivo o quale tipo di oggetto viene restituito package).

Sarebbe quindi desiderabile utilizzare in qualche modo questo dispositivo in una funzione di test in modo tale da poter anche specificare l' versionargomento di quel dispositivo da utilizzare con quel test. Questo non è attualmente possibile, anche se potrebbe essere una bella funzionalità.

Nel frattempo è stato abbastanza facile fare in modo che il mio dispositivo restituisse semplicemente una funzione che fa tutto il lavoro svolto in precedenza dal dispositivo, ma mi permette di specificare l' versionargomento:

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

    return make_test_package

Ora posso usarlo nella mia funzione di test come:

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

e così via.

La soluzione tentata dall'OP andava nella giusta direzione e, come suggerisce la risposta di @ hpk42 , MyTester.__init__potrebbe semplicemente memorizzare un riferimento alla richiesta come:

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"

Quindi usalo per implementare il dispositivo come:

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

Se lo si desidera, la MyTesterclasse potrebbe essere leggermente ristrutturata in modo che il suo .argsattributo possa essere aggiornato dopo che è stato creato, per modificare il comportamento per i singoli test.


Grazie per il suggerimento con la funzione all'interno dell'apparecchio. Ci è voluto del tempo prima che potessi lavorarci di nuovo, ma è piuttosto utile!
maggie


non ricevi un errore che dice: "I dispositivi non sono pensati per essere chiamati direttamente, ma vengono creati automaticamente quando le funzioni di test li richiedono come parametri."?
nz_21

153

Questo è attualmente supportato nativamente in py.test tramite parametrizzazione indiretta .

Nel tuo caso, avresti:

@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

Ah, questo è piuttosto carino (penso che il tuo esempio potrebbe essere un po 'obsoleto, però - differisce dagli esempi nei documenti ufficiali). È una funzionalità relativamente nuova? Non l'ho mai incontrato prima. Questa è anche una buona soluzione al problema, per certi versi migliore della mia risposta.
Iguananaut,

2
Ho provato a utilizzare questa soluzione ma ho riscontrato problemi nel passaggio di più parametri o nell'utilizzo di nomi di variabili diversi dalla richiesta. Ho finito per usare la soluzione di @Iguananaut.
Victor Uriarte

42
Questa dovrebbe essere la risposta accettata. La documentazione ufficiale per l' indirectargomento della parola chiave è certamente scarsa e poco amichevole, il che probabilmente spiega l'oscurità di questa tecnica essenziale. Ho setacciato il sito py.test in più occasioni per questa caratteristica, solo per apparire vuoto, più vecchio e confuso. L'amarezza è un luogo noto come integrazione continua. Grazie a Odin per Stackoverflow.
Cecil Curry

1
Nota che questo metodo cambia il nome dei tuoi test per includere il parametro, che può essere desiderato o meno. test_tc1diventa test_tc1[tester0].
jjj

1
Quindi indirect=Truepassa i parametri a tutti i dispositivi chiamati, giusto? Perché la documentazione nomina esplicitamente i dispositivi per la parametrizzazione indiretta, ad esempio per un dispositivo denominato x:indirect=['x']
winklerrr

11

È possibile accedere al modulo / classe / funzione richiedente dalle funzioni del dispositivo (e quindi dalla classe Tester), vedere interagire con il contesto di test richiesto da una funzione del dispositivo . Quindi è possibile dichiarare alcuni parametri su una classe o un modulo e l'apparecchiatura del tester può prenderlo.


3
So di poter fare qualcosa del genere: (dalla documentazione) @ pytest.fixture (scope = "module", params = ["merlinux.eu", "mail.python.org"]) Ma devo farlo in il modulo di test. Come posso aggiungere dinamicamente i parametri ai fari?
maggie

2
Il punto non è quello di dover interagire con la richiesta del contesto di test da una funzione di dispositivo, ma di avere un modo ben definito per passare argomenti a una funzione di dispositivo. La funzione dispositivo non dovrebbe essere a conoscenza di un tipo di contesto di test richiesto solo per essere in grado di ricevere argomenti con nomi concordati. Ad esempio, si vorrebbe essere in grado di scrivere @fixture def my_fixture(request)e poi inserire @pass_args(arg1=..., arg2=...) def test(my_fixture)questi argomenti in my_fixture()questo modo arg1 = request.arg1, arg2 = request.arg2. È possibile qualcosa del genere in py.test adesso?
Piotr Dobrogost

7

Non sono riuscito a trovare alcun documento, tuttavia, sembra funzionare nell'ultima versione di 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

Grazie per averlo sottolineato: questa sembra la soluzione più pulita di tutte. Non penso che questo fosse possibile nelle versioni precedenti, ma è chiaro che ora lo è. Sai se questo modulo è menzionato da qualche parte nei documenti ufficiali ? Non sono riuscito a trovare niente di simile, ma chiaramente funziona. Ho aggiornato la mia risposta per includere questo esempio, grazie.
Iguananaut,

1
Penso che non sarà possibile nella funzionalità, se dai un'occhiata a github.com/pytest-dev/pytest/issues/5712 e al relativo PR (unito).
Nadège,

Questo è stato ripristinato github.com/pytest-dev/pytest/pull/6914
Maspe36

1
Per chiarire, @ Maspe36 indica che il PR collegato da è Nadègestato ripristinato. Quindi, questa caratteristica non documentata (penso che sia ancora priva di documenti?) Vive ancora.
blthayer

6

Per migliorare un po ' la risposta di imiric : un altro modo elegante per risolvere questo problema è creare "dispositivi di parametri". Personalmente lo preferisco alla indirectfunzionalità di pytest. Questa funzione è disponibile da pytest_cases, e l'idea originale è stata suggerita da 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

Si noti che pytest-casesfornisce anche @pytest_fixture_plusche consentono di utilizzare i contrassegni di parametrizzazione sui dispositivi e @cases_datache consentono di acquisire i parametri dalle funzioni in un modulo separato. Vedere documento per i dettagli. A proposito, io sono l'autore;)


1
Questo sembra funzionare anche in plain pytest (ho la v5.3.1). Cioè, sono stato in grado di farlo funzionare senza param_fixture. Vedi questa risposta . Tuttavia, non sono riuscito a trovare alcun esempio simile nei documenti; ne sai qualcosa?
Iguananaut,

grazie per le informazioni e il link! Non avevo idea che fosse fattibile. Aspettiamo una documentazione ufficiale per vedere cosa hanno in mente.
smarie

2

Ho creato un decoratore divertente che consente di scrivere dispositivi come questo:

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

Qui, a sinistra di /voi ci sono altri proiettori, ea destra ci sono i parametri che vengono forniti utilizzando:

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

Funziona allo stesso modo degli argomenti delle funzioni. Se non si fornisce l' ageargomento 69, viene utilizzato quello predefinito ,,. se non fornisci nameo ometti il dog.argumentsdecoratore, ottieni il normale TypeError: dog() missing 1 required positional argument: 'name'. Se hai un altro dispositivo che accetta argomenti name, non è in conflitto con questo.

Sono supportati anche i dispositivi asincroni.

Inoltre, questo ti offre un bel piano di installazione:

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

Un esempio completo:

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"

Il codice per il decoratore:

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.