Fikstür işlevine bir parametre geçirin


114

Bir python sınıfı MyTester ile sarılmış bazı DLL kodunu test etmek için py.test kullanıyorum. Doğrulama amacıyla, testler sırasında bazı test verilerini kaydetmem ve sonrasında daha fazla işlem yapmam gerekiyor. Birçok test _... dosyam olduğundan, testlerimin çoğu için test cihazı nesnesi oluşturmayı (MyTester örneği) yeniden kullanmak istiyorum.

Test nesnesi, DLL değişkenlerine ve işlevlerine referansları alan nesne olduğundan, test dosyalarının her biri için test edici nesneye DLL değişkenlerinin bir listesini iletmem gerekiyor (günlüğe kaydedilecek değişkenler bir test_ .. için aynıdır. . dosya). Listenin içeriği, belirtilen verileri kaydetmek için kullanılacaktır.

Benim fikrim bunu bir şekilde yapmak:

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

Bunu böyle başarmak mümkün mü yoksa daha zarif bir yolu var mı?

Genellikle bunu her test yöntemi için bir tür kurulum işleviyle (xUnit tarzı) yapabilirim. Ama bir tür yeniden kullanım kazanmak istiyorum. Bunun demirbaşlarla mümkün olup olmadığını bilen var mı?

Bunun gibi bir şey yapabileceğimi biliyorum: (dokümanlardan)

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

Ancak doğrudan test modülünde parametreleştirmeye ihtiyacım var. Fikstürün params niteliğine test modülünden erişmek mümkün mü?

Yanıtlar:


101

Güncelleme: Bu sorunun cevabı kabul edildiğinden ve bazen hala olumlu oy verildiğinden, bir güncelleme eklemeliyim. Orijinal cevabım (aşağıda), başkalarının da belirttiği gibi, pytest'in eski sürümlerinde bunu yapmanın tek yolu olmasına rağmen , pytest artık armatürlerin dolaylı parametrizasyonunu desteklemektedir. Örneğin, bunun gibi bir şey yapabilirsiniz (@imiric aracılığıyla):

# 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

Ancak, dolaylı parametrelemesine bu formu @Yukihiko Shinoda olarak, açık olmasına rağmen işaret (ı resmi dokümanlar bu herhangi bariz bir başvuru bulamadık rağmen) şimdi örtülü dolaylı parametrelemesine biçimi destekler:

# 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

Bu formun anlamsallığının tam olarak ne olduğunu bilmiyorum, ancak görünen o pytest.mark.parametrizeki, test_tc1yöntem adlandırılmış bir argüman almasa da tester_arg, kullandığı testerfikstür, parametreleştirilmiş argümanı testerfikstürden geçiriyor.


Benzer bir sorunum vardı - adlı bir fikstürüm var test_packageve daha sonra belirli testlerde çalıştırırken o fikstüre isteğe bağlı bir argüman iletebilmek istedim. Örneğin:

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

(Bu amaçlar için fikstürün ne yaptığı veya ne tür bir nesne olduğu önemli değildir package).

O zaman bu armatürü bir şekilde bir test fonksiyonunda kullanmak, öyle ki ben de şunu belirtebilirim. version o testte kullanmak için o fikstürün argümanını . Bu şu anda mümkün değil, ancak güzel bir özellik olabilir.

Bu arada , fikstürümün, fikstürün daha önce yaptığı tüm işi yapan, ancak argümanı belirtmeme izin veren bir işlevi döndürmesini sağlamak yeterince kolaydı version:

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

    return make_test_package

Şimdi bunu test işlevimde şu şekilde kullanabilirim:

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

ve bunun gibi.

OP'nin denenen çözümü doğru yöne gidiyordu ve @ hpk42'nin cevabının önerdiği gibi, MyTester.__init__sadece aşağıdaki gibi isteğe bir referans kaydedebilirdi:

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"

Ardından, fikstürü aşağıdaki gibi uygulamak için bunu kullanın:

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

İstenirse, MyTestersınıf biraz yeniden yapılandırılabilir, böylece .argsözniteliği oluşturulduktan sonra bireysel testler için davranışı ince ayarlayacak şekilde güncellenebilir.


Armatürün içindeki fonksiyonla ilgili ipucu için teşekkürler. Bunun üzerinde tekrar çalışabilmem biraz zaman aldı ama bu oldukça faydalı!
maggie

2
Bu konuyla ilgili güzel bir kısa gönderi: alysivji.github.io/pytest-fixures-with-function-arguments.html
maggie

"Fikstürlerin doğrudan çağrılması amaçlanmamıştır, ancak test fonksiyonları bunları parametre olarak talep ettiğinde otomatik olarak oluşturulur." şeklinde bir hata almıyor musunuz?
nz_21

154

Bu aslında dolaylı parametrelendirme yoluyla py.test'te yerel olarak desteklenir .

Senin durumunda, sahip olacaksın:

@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, bu oldukça hoş (Örneğinizin biraz modası geçmiş olabileceğini düşünüyorum - resmi belgelerdeki örneklerden farklı). Bu nispeten yeni bir özellik mi? Daha önce hiç karşılaşmadım. Bu da soruna iyi bir çözüm - bazı açılardan cevabımdan daha iyi.
Iguananaut

2
Bu çözümü kullanmayı denedim, ancak birden çok parametreyi geçerken veya istek dışında değişken adları kullanmada sorunlar yaşıyordum. @Iguananaut'un çözümünü kullandım.
Victor Uriarte

42
Kabul edilen cevap bu olmalıdır. Resmi belgeler için indirectanahtar kelime argüman muhtemelen bu temel tekniğin belirsizlik hesapları, hangi kuşkusuz seyrek ve düşmanca. Py.test sitesini bu özellik için birçok kez araştırdım - yalnızca boş, daha eski ve şaşkın bir şekilde ortaya çıkması için. Acılık, sürekli entegrasyon olarak bilinen bir yerdir. Stackoverflow için Odin'e teşekkürler.
Cecil Curry

1
Bu yöntemin, testlerinizin adını, istenebilecek veya olmayabilecek parametreyi içerecek şekilde değiştirdiğini unutmayın. test_tc1olur test_tc1[tester0].
jjj

1
Yani indirect=Trueparametreleri çağrılan tüm armatürlere devredin, değil mi? Çünkü belgeler adında bir fikstür için adı açıkça örn dolaylı parametreleriyle için demirbaşlar x:indirect=['x']
winklerrr

11

İstenen modüle / sınıfa / işleve fikstür işlevlerinden (ve dolayısıyla Tester sınıfınızdan) erişebilirsiniz, bir fikstür işlevinden istekte bulunan test içeriği ile etkileşime bakın . Böylece bir sınıf veya modül üzerinde bazı parametreler tanımlayabilirsiniz ve test cihazı bunu alabilir.


3
Bunun gibi bir şey yapabileceğimi biliyorum: (dokümanlardan) @ pytest.fixture (kapsam = "modül", params = ["merlinux.eu", "mail.python.org"]) Ama bunu şurada yapmam gerekiyor test modülü. Fikstürlere dinamik olarak nasıl parametre ekleyebilirim?
maggie

2
Önemli olan , bir fikstür işlevinden test içeriği talep etmekle etkileşime girmek zorunda değil, argümanları bir fikstür işlevine iletmek için iyi tanımlanmış bir yola sahip olmaktır. Fikstür işlevinin, yalnızca üzerinde anlaşmaya varılan adlara sahip argümanları alabilmek için bir tür test bağlamından haberdar olması gerekmez. Mesela yazıp @fixture def my_fixture(request)sonra @pass_args(arg1=..., arg2=...) def test(my_fixture)bu argümleri bu şekilde almak my_fixture()ister arg1 = request.arg1, arg2 = request.arg2. Py.test'te böyle bir şey şimdi mümkün mü?
Piotr Dobrogost

7

Herhangi bir belge bulamadım, ancak pytest'in son sürümünde çalışıyor gibi görünüyor.

@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

Bunu belirttiğiniz için teşekkürler - bu en temiz çözüm gibi görünüyor. Bunun daha önceki sürümlerde mümkün olduğunu sanmıyorum, ancak şimdi olduğu açık. Resmi belgelerin herhangi bir yerinde bu formdan bahsedilip bahsedilmediğini biliyor musunuz ? Tam olarak böyle bir şey bulamadım, ama açıkça işe yarıyor. Cevabımı bu örneği içerecek şekilde güncelledim , teşekkürler.
Iguananaut

1
Github.com/pytest-dev/pytest/issues/5712 ve ilgili (birleştirilmiş) PR'ye bakarsanız, özellikte bunun mümkün olmayacağını düşünüyorum .
Nadège


1
Açıklığa kavuşturmak için, @ Maspe36, tarafından bağlanan PR'nin Nadègegeri alındığını gösteriyor. Dolayısıyla, bu belgelenmemiş özellik (sanırım hala belgelenmemiş mi?) Hala yaşıyor.
blthayer

6

Biraz geliştirmek için imiric cevabını : Bu sorunu çözmenin bir başka şık yolu da "parametre fikstürleri" yaratmaktır. Ben şahsen indirectözelliğine tercih ederim pytest. Bu özellik, adresinden edinilebilirpytest_cases ve orijinal fikir Sup3rGeo tarafından önerilmiştir .

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

Bunu not et pytest-cases ayrıca sağlayan @pytest_fixture_plusbu sizin armatürleri parametrization işaretlerini kullanmasına izin ve @cases_dataayrı bir modülde işlevlerden Parametrelerinizi kaynak sağlamasıdır. Ayrıntılar için belgeye bakın. Bu arada ben yazarım;)


1
Bu şimdi düz pytest'te de çalışıyor gibi görünüyor (v5.3.1'e sahibim). Yani, bunu onsuz çalıştırabildim param_fixture. Bu cevaba bakın . Dokümanlarda buna benzer bir örnek bulamadım; bunun hakkında bir şey biliyor musun
Iguananaut

bilgi ve bağlantı için teşekkürler! Bunun mümkün olduğunu bilmiyordum. Akıllarında ne olduğunu görmek için resmi bir belge bekleyelim.
smarie

2

Bunun gibi armatürlerin yazılmasına izin veren komik bir dekoratör yaptım:

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

Burada, solunuzda /başka armatürler var ve sağda, aşağıdakileri kullanarak sağlanan parametreleriniz var:

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

Bu, işlev bağımsız değişkenlerinin çalıştığı şekilde çalışır. Bağımsız agedeğişken sağlamazsanız 69, bunun yerine varsayılan olan kullanılır. Dekoratörü tedarik etmezseniz nameveya ihmal dog.argumentsederseniz, normal olanı alırsınız TypeError: dog() missing 1 required positional argument: 'name'. Tartışmayı alan başka bir fikstürünüz namevarsa, bununla çelişmez.

Asenkron fikstürler de desteklenmektedir.

Ek olarak, bu size güzel bir kurulum planı sağlar:

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

Tam bir örnek:

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"

Dekoratörün kodu:

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
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.