Passe um parâmetro para uma função de fixação

114

Estou usando o py.test para testar alguns códigos DLL incluídos em uma classe Python MyTester. Para fins de validação, preciso registrar alguns dados de teste durante os testes e fazer mais processamento depois. Como tenho muitos arquivos de teste _..., desejo reutilizar a criação do objeto testador (instância de MyTester) para a maioria dos meus testes.

Como o objeto testador é aquele que obteve as referências às variáveis ​​e funções da DLL, preciso passar uma lista das variáveis ​​da DLL ao objeto testador para cada um dos arquivos de teste (as variáveis ​​a serem registradas são as mesmas para um test_ .. . Arquivo). O conteúdo da lista deve ser usado para registrar os dados especificados.

Minha ideia é fazer de alguma forma assim:

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

É possível conseguir assim ou existe uma forma ainda mais elegante?

Normalmente eu poderia fazer isso para cada método de teste com algum tipo de função de configuração (estilo xUnit). Mas quero obter algum tipo de reutilização. Alguém sabe se isso é possível com luminárias?

Eu sei que posso fazer algo assim: (dos documentos)

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

Mas preciso fazer a parametrização diretamente no módulo de teste. É possível acessar o atributo params do aparelho a partir do módulo de teste?

Maggie
fonte

Respostas:

101

Atualização: uma vez que esta é a resposta aceita para esta pergunta e ainda recebe votos positivos às vezes, devo adicionar uma atualização. Embora a minha resposta original (abaixo) foi a única maneira de fazer isso em versões mais antigas do pytest como outrosobservou pytest agora suporta parametrização indireta de acessórios. Por exemplo, você pode fazer algo assim (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

No entanto, embora essa forma de parametrização indireta seja explícita, como @Yukihiko Shinoda aponta, ela agora suporta uma forma de parametrização indireta implícita (embora eu não tenha conseguido encontrar nenhuma referência óbvia a isso nos documentos oficiais):

# 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

Não sei exatamente quais são as semânticas desse formulário, mas parece que pytest.mark.parametrizereconhece que embora o test_tc1método não receba um argumento chamado tester_arg, o testeracessório que está usando o faz, então ele passa o argumento parametrizado pelo testeracessório.


Eu tive um problema semelhante - eu tenho um acessório chamado test_packagee, mais tarde, quis ser capaz de passar um argumento opcional para aquele acessório ao executá-lo em testes específicos. Por exemplo:

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

(Não importa para esses propósitos o que o aparelho faz ou que tipo de objeto retornado package) é.

Então, seria desejável usar de alguma forma esse acessório em uma função de teste de forma que eu também pudesse especificar o versionargumento daquele acessório para usar com esse teste. No momento, isso não é possível, embora possa ser um bom recurso.

Nesse ínterim, foi fácil fazer meu aparelho simplesmente retornar uma função que faz todo o trabalho que o aparelho fazia anteriormente, mas me permite especificar o versionargumento:

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

    return make_test_package

Agora posso usar isso em minha função de teste, como:

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

e assim por diante.

A tentativa de solução do OP estava indo na direção certa e, como sugere a resposta de @ hpk42 , ele MyTester.__init__poderia simplesmente armazenar uma referência à solicitação como:

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"

Em seguida, use isso para implementar o acessório como:

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

Se desejado, a MyTesterclasse pode ser reestruturada um pouco para que seu .argsatributo possa ser atualizado depois de ter sido criado, para ajustar o comportamento para testes individuais.

Iguananaut
fonte
Obrigado pela dica com a função dentro do aparelho. Demorou algum tempo até que eu pudesse trabalhar nisso novamente, mas isso é muito útil!
maggie
2
Uma boa postagem curta sobre este tópico: alysivji.github.io/pytest-fixures-with-function-arguments.html
maggie
você não está recebendo um erro dizendo: "As luminárias não devem ser chamadas diretamente, mas são criadas automaticamente quando as funções de teste as solicitam como parâmetros."?
nz_21
153

Na verdade, isso é suportado nativamente em py.test por meio de parametrização indireta .

No seu caso, você teria:

@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
imírico
fonte
Ah, isso é muito bom (acho que seu exemplo pode estar um pouco desatualizado - é diferente dos exemplos nos documentos oficiais). Este é um recurso relativamente novo? Eu nunca encontrei isso antes. Essa também é uma boa solução para o problema - em alguns aspectos, melhor do que a minha resposta.
Iguananaut
2
Tentei usar esta solução, mas estava tendo problemas para passar vários parâmetros ou usar nomes de variáveis ​​diferentes de request. Acabei usando a solução da @Iguananaut.
Victor Uriarte
42
Esta deve ser a resposta aceita. A documentação oficial para o indirectargumento da palavra - chave é reconhecidamente esparsa e hostil, o que provavelmente explica a obscuridade desta técnica essencial. Eu vasculhei o site py.test em várias ocasiões em busca desse recurso - apenas para aparecer vazio, mais antigo e confuso. A amargura é um lugar conhecido como integração contínua. Obrigado Odin pelo Stackoverflow.
Cecil Curry
1
Observe que esse método altera o nome de seus testes para incluir o parâmetro, que pode ou não ser desejado. test_tc1torna-se test_tc1[tester0].
jjj
1
Então indirect=Truepassa os parâmetros para todos os aparelhos chamados, certo? Porque a documentação nomeia explicitamente os aparelhos para parametrização indireta, por exemplo, para um aparelho chamado x:indirect=['x']
winklerrr
11

Você pode acessar o módulo / classe / função solicitante de funções de fixture (e, portanto, de sua classe de testador), consulte interação com o contexto de teste de solicitação de uma função de fixture . Portanto, você pode declarar alguns parâmetros em uma classe ou módulo e o equipamento do testador pode pegá-lo.

hpk42
fonte
3
Eu sei que posso fazer algo assim: (dos documentos) @ pytest.fixture (scope = "module", params = ["merlinux.eu", "mail.python.org"]) Mas eu preciso fazer isso em o módulo de teste. Como posso adicionar parâmetros dinamicamente às luminárias?
maggie
2
A questão não é ter que interagir com a solicitação de contexto de teste de uma função de fixture, mas ter uma maneira bem definida de passar argumentos para uma função de fixture. A função de fixação não deve estar ciente de um tipo de contexto de teste solicitante apenas para ser capaz de receber argumentos com nomes combinados. Por exemplo, alguém gostaria de ser capaz de escrever @fixture def my_fixture(request)e, em seguida, @pass_args(arg1=..., arg2=...) def test(my_fixture)obter esses argumentos my_fixture()assim arg1 = request.arg1, arg2 = request.arg2. Algo assim é possível em py.test agora?
Piotr Dobrogost
7

Não consegui encontrar nenhum documento, no entanto, parece funcionar na versão mais recente do 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
fonte
Obrigado por apontar isso - esta parece ser a solução mais limpa de todas. Não acho que isso era possível nas versões anteriores, mas está claro que agora é. Você sabe se este formulário é mencionado em algum lugar dos documentos oficiais ? Não consegui encontrar nada parecido, mas claramente funciona. Eu atualizei minha resposta para incluir este exemplo, obrigado.
Iguananaut
1
Acho que não será possível no recurso, se você der uma olhada em github.com/pytest-dev/pytest/issues/5712 e o PR relacionado (mesclado).
Nadège
Isso foi revertido github.com/pytest-dev/pytest/pull/6914
Maspe36
1
Para esclarecer, @ Maspe36 está indicando que o PR vinculado por Nadègefoi revertido. Portanto, esse recurso não documentado (acho que ainda é ilegal?) Ainda vive.
blthayer
6

Para melhorar um pouco a resposta de imiric : outra forma elegante de resolver este problema é criar "luminárias de parâmetros". Eu pessoalmente prefiro isso em vez do indirectrecurso de pytest. Este recurso está disponível em pytest_casese a ideia original foi sugerida por 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

Observe que pytest-casestambém fornece @pytest_fixture_plusque permitem que você use marcas de parametrização em seus aparelhos de iluminação e @cases_dataque permitem que você forneça seus parâmetros de funções em um módulo separado. Veja doc para detalhes. A propósito, sou o autor;)

smarie
fonte
1
Isso parece funcionar agora em pytest simples também (eu tenho v5.3.1). Ou seja, consegui fazer isso funcionar sem param_fixture. Veja esta resposta . Não consegui encontrar nenhum exemplo semelhante na documentação; Você sabe algo sobre isso?
Iguananaut
obrigado pela informação e pelo link! Eu não tinha ideia de que isso era viável. Vamos aguardar uma documentação oficial para ver o que eles têm em mente.
smarie
2

Fiz um decorador engraçado que permite escrever acessórios como este:

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

Aqui, à esquerda de /você tem outros acessórios, e à direita você tem parâmetros que são fornecidos usando:

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

Isso funciona da mesma maneira que os argumentos de função. Se você não fornecer o ageargumento, o padrão,, 69será usado. se você não fornecer nameou omitir o dog.argumentsdecorador, você receberá o regular TypeError: dog() missing 1 required positional argument: 'name'. Se você tiver outro aparelho que requer argumento name, ele não entra em conflito com este.

Dispositivos assíncronos também são suportados.

Além disso, isso oferece um bom plano de configuração:

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

Um exemplo 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"

O código para o decorador:

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
esquilo
fonte