Meneruskan parameter ke fungsi fixture

114

Saya menggunakan py.test untuk menguji beberapa kode DLL yang dibungkus dengan kelas python MyTester. Untuk tujuan validasi, saya perlu mencatat beberapa data pengujian selama pengujian dan melakukan lebih banyak pemrosesan setelahnya. Karena saya memiliki banyak file uji _..., saya ingin menggunakan kembali pembuatan objek penguji (contoh MyTester) untuk sebagian besar pengujian saya.

Karena objek penguji adalah yang mendapat referensi ke variabel dan fungsi DLL, saya harus meneruskan daftar variabel DLL ke objek penguji untuk masing-masing file pengujian (variabel yang akan dicatat sama untuk pengujian_ .. file). Isi dari daftar akan digunakan untuk mencatat data yang ditentukan.

Ide saya adalah melakukannya dengan cara seperti ini:

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

Apakah mungkin untuk mencapainya seperti ini atau bahkan ada cara yang lebih elegan?

Biasanya saya bisa melakukannya untuk setiap metode pengujian dengan beberapa jenis fungsi pengaturan (gaya xUnit). Tapi saya ingin mendapatkan semacam penggunaan kembali. Adakah yang tahu apakah ini mungkin dengan perlengkapan sama sekali?

Saya tahu saya bisa melakukan sesuatu seperti ini: (dari dokumen)

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

Tapi saya perlu parametrization langsung di modul tes. Apakah mungkin untuk mengakses atribut params fixture dari modul tes?

maggie
sumber

Jawaban:

101

Pembaruan: Karena ini adalah jawaban yang diterima untuk pertanyaan ini dan kadang-kadang masih mendapat suara positif, saya harus menambahkan pembaruan. Meskipun jawaban asli saya (di bawah) adalah satu-satunya cara untuk melakukan ini di versi pytest yang lebih lama karena yang lain telah mencatat pytest sekarang mendukung parametriisasi perlengkapan tidak langsung. Misalnya Anda dapat melakukan sesuatu seperti ini (melalui @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

Namun, meskipun bentuk parametrization tidak langsung adalah eksplisit, seperti @Yukihiko Shinoda menunjukkan sekarang mendukung bentuk parametrization tidak langsung implisit (meskipun saya tidak bisa menemukan referensi yang jelas untuk ini di dokumentasi resmi):

# 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

Saya tidak tahu persis apa semantik dari formulir ini, tetapi tampaknya pytest.mark.parametrizemengakui bahwa meskipun test_tc1metode tidak mengambil argumen bernama tester_arg, testerfixture yang digunakan tidak, sehingga meneruskan argumen parametrized melalui testerfixture.


Saya memiliki masalah yang sama - saya memiliki fixture yang dipanggil test_package, dan saya kemudian ingin memberikan argumen opsional ke fixture tersebut saat menjalankannya dalam tes tertentu. Sebagai contoh:

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

(Tidak masalah untuk tujuan ini apa yang dilakukan fixture atau jenis objek yang dikembalikan package).

Maka akan diinginkan untuk entah bagaimana menggunakan perlengkapan ini dalam fungsi uji sedemikian rupa sehingga saya juga dapat menentukan versionargumen ke perlengkapan itu untuk digunakan dengan tes itu. Ini saat ini tidak memungkinkan, meskipun mungkin merupakan fitur yang bagus.

Sementara itu, cukup mudah untuk membuat perlengkapan saya mengembalikan fungsi yang melakukan semua pekerjaan fixture sebelumnya, tetapi memungkinkan saya untuk menentukan versionargumen:

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

    return make_test_package

Sekarang saya dapat menggunakan ini dalam fungsi pengujian saya seperti:

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

dan seterusnya.

Solusi yang dicoba OP sedang menuju ke arah yang benar, dan seperti yang disarankan oleh jawaban @ hpk42 , MyTester.__init__itu hanya dapat menyimpan referensi ke permintaan seperti:

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"

Kemudian gunakan ini untuk mengimplementasikan perlengkapan seperti:

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

Jika diinginkan, MyTesterkelas dapat direstrukturisasi sedikit sehingga .argsatributnya dapat diperbarui setelah dibuat, untuk menyesuaikan perilaku pengujian individual.

Iguananaut
sumber
Terima kasih atas petunjuknya dengan fungsi di dalam fixture. Butuh beberapa waktu sampai saya bisa mengerjakan ini lagi tetapi ini sangat berguna!
maggie
2
Sebuah posting singkat yang bagus tentang topik ini: alysivji.github.io/pytest-fixures-with-function-arguments.html
maggie
apakah Anda tidak mendapatkan pesan kesalahan yang mengatakan: "Fixtures tidak dimaksudkan untuk dipanggil secara langsung, tetapi dibuat secara otomatis ketika fungsi pengujian memintanya sebagai parameter."?
nz_21
153

Ini sebenarnya didukung secara native di py.test melalui parametrization tidak langsung .

Dalam kasus Anda, Anda akan memiliki:

@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
imiric
sumber
Ah, ini cukup bagus (saya pikir contoh Anda mungkin agak ketinggalan jaman - ini berbeda dari contoh di dokumen resmi). Apakah ini fitur yang relatif baru? Saya belum pernah bertemu sebelumnya. Ini adalah solusi yang bagus untuk masalah ini juga - dalam beberapa hal lebih baik daripada jawaban saya.
Iguananaut
2
Saya mencoba menggunakan solusi ini tetapi mengalami masalah melewati beberapa parameter atau menggunakan nama variabel selain permintaan. Saya akhirnya menggunakan solusi @Iguananaut.
Victor Uriarte
42
Ini harus menjadi jawaban yang diterima. The dokumentasi resmi untuk indirectargumen kata kunci diakui jarang dan tidak ramah, yang mungkin account untuk ketidakjelasan teknik penting ini. Saya telah menjelajahi situs py.test pada beberapa kesempatan untuk fitur ini - hanya untuk tampil kosong, lebih tua, dan bingung. Kepahitan adalah tempat yang dikenal sebagai integrasi berkelanjutan. Terima kasih Odin untuk Stackoverflow.
Cecil Curry
1
Perhatikan bahwa metode ini mengubah nama pengujian Anda untuk menyertakan parameter, yang mungkin diinginkan atau tidak diinginkan. test_tc1menjadi test_tc1[tester0].
jjj
1
Jadi indirect=Trueserahkan parameter ke semua perlengkapan yang disebut, bukan? Karena dokumentasi secara eksplisit menamai perlengkapan untuk parametrikisasi tidak langsung, misalnya untuk perlengkapan bernama x:indirect=['x']
winklerrr
11

Anda dapat mengakses modul / kelas / fungsi yang meminta dari fungsi perlengkapan (dan dengan demikian dari kelas Penguji Anda), lihat berinteraksi dengan meminta konteks pengujian dari fungsi perlengkapan . Jadi Anda dapat mendeklarasikan beberapa parameter pada kelas atau modul dan perlengkapan penguji dapat mengambilnya.

hpk42.dll
sumber
3
Saya tahu saya dapat melakukan sesuatu seperti ini: (dari dokumen) @ pytest.fixture (scope = "module", params = ["merlinux.eu", "mail.python.org"]) Tetapi saya perlu melakukannya di modul tes. Bagaimana cara saya menambahkan params ke perlengkapan secara dinamis?
maggie
2
Intinya bukanlah harus berinteraksi dengan meminta konteks pengujian dari fungsi fixture tetapi memiliki cara yang terdefinisi dengan baik untuk meneruskan argumen ke fungsi fixture. Fungsi fixture seharusnya tidak mengetahui jenis konteks pengujian yang meminta hanya untuk dapat menerima argumen dengan nama yang disepakati. Misalnya seseorang ingin bisa menulis @fixture def my_fixture(request)dan kemudian @pass_args(arg1=..., arg2=...) def test(my_fixture)mendapatkan argumen my_fixture()seperti ini arg1 = request.arg1, arg2 = request.arg2. Apakah hal seperti ini mungkin terjadi di py.test sekarang?
Piotr Dobrogost
7

Saya tidak dapat menemukan dokumen apa pun, namun tampaknya berfungsi di versi terbaru 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
sumber
Terima kasih telah menunjukkan hal ini - ini sepertinya solusi terbersih dari semuanya. Saya tidak berpikir ini dulunya mungkin di versi sebelumnya, tetapi jelas sekarang. Tahukah Anda jika formulir ini disebutkan di mana pun di dokumen resmi ? Saya tidak dapat menemukan yang seperti itu, tetapi jelas berfungsi. Saya telah memperbarui jawaban saya untuk memasukkan contoh ini, terima kasih.
Iguananaut
1
Saya pikir itu tidak akan mungkin dalam fitur, jika Anda melihat di github.com/pytest-dev/pytest/issues/5712 dan PR (gabungan) terkait.
Nadège
Ini dikembalikan github.com/pytest-dev/pytest/pull/6914
Maspe36
1
Untuk memperjelas, @ Maspe36 menyatakan bahwa PR yang ditautkan oleh Nadègetelah dikembalikan. Jadi, fitur tidak berdokumen ini (menurut saya masih tidak terdokumentasi?) Masih hidup.
Blthayer
6

Untuk meningkatkan jawaban imiric sedikit : cara elegan lain untuk memecahkan masalah ini adalah dengan membuat "perlengkapan parameter". Saya pribadi lebih suka indirectfitur ini daripada pytest. Fitur ini tersedia dari pytest_cases, dan ide aslinya disarankan oleh 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

Perhatikan bahwa pytest-casesjuga menyediakan @pytest_fixture_plusyang memungkinkan Anda untuk menggunakan tanda parametrization pada perlengkapan Anda, dan @cases_datayang memungkinkan Anda untuk sumber parameter Anda dari fungsi dalam modul terpisah. Lihat dok untuk detailnya. Ngomong-ngomong, saya adalah penulisnya;)

smarie
sumber
1
Ini tampaknya bekerja sekarang di pytest biasa juga (saya punya v5.3.1). Artinya, saya bisa mendapatkan ini bekerja tanpa param_fixture. Lihat jawaban ini . Saya tidak bisa menemukan contoh seperti itu di dokumen; apakah kamu tahu sesuatu tentang ini?
Iguananaut
terima kasih untuk infonya dan linknya! Saya tidak tahu ini mungkin. Mari kita tunggu dokumentasi resmi untuk melihat apa yang mereka pikirkan.
smarie
2

Saya membuat dekorator lucu yang memungkinkan perlengkapan menulis seperti ini:

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

Di sini, di sebelah kiri /Anda memiliki perlengkapan lain, dan di sebelah kanan Anda memiliki parameter yang disediakan menggunakan:

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

Ini bekerja dengan cara yang sama seperti fungsi argumen bekerja. Jika Anda tidak memberikan ageargumen, argumen default 69,, akan digunakan. jika Anda tidak menyediakan name, atau menghilangkan dog.argumentsdekorator, Anda mendapatkan yang biasa TypeError: dog() missing 1 required positional argument: 'name'. Jika Anda memiliki perlengkapan lain yang membutuhkan argumenname , itu tidak bertentangan dengan yang satu ini.

Perlengkapan Async juga didukung.

Selain itu, ini memberi Anda rencana penyiapan yang bagus:

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

Contoh lengkapnya:

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"

Kode untuk dekorator:

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
tupai
sumber