パラメータをフィクスチャ関数に渡す


114

私はpy.testを使用して、PythonクラスMyTesterにラップされたDLLコードをテストしています。検証目的で、テスト中にいくつかのテストデータをログに記録し、後でさらに処理を行う必要があります。多くのtest _...ファイルがあるので、ほとんどのテストでテスターオブジェクトの作成(MyTesterのインスタンス)を再利用したいと思います。

テスターオブジェクトはDLLの変数と関数への参照を取得したオブジェクトであるため、各テストファイルのDLL変数のリストをテスターオブジェクトに渡す必要があります(ログに記録される変数はtest_ ..でも同じです。 。ファイル)。リストの内容は、指定されたデータをログに記録するために使用されます。

私の考えは、このようにすることです。

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

このようにそれを達成することは可能ですか、それとももっとエレガントな方法がありますか?

通常は、ある種のセットアップ関数(xUnitスタイル)を使用して、テストメソッドごとに実行できます。しかし、私は何らかの再利用をしたいです。フィクスチャでこれが可能かどうか誰かが知っていますか?

私はこのようなことができることを知っています:(ドキュメントから)

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

しかし、テストモジュールで直接パラメーター化する必要があります。 テストモジュールからフィクスチャのparams属性にアクセスできますか?

回答:


100

更新:これはこの質問に対する受け入れられた回答であり、それでも時々賛成されるので、更新を追加する必要があります。以前のバージョンのpytestでこれを行うには、元の答え(下記)が唯一の方法でしたが、pytestがフィクスチャの間接的なパラメーター化をサポートするようになったこと他の人指摘しています。たとえば、(@ 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

ただし、この間接的なパラメーター化の形式は明示的ですが、@ Yukihiko Shinodaが指摘するように、暗黙的な間接的なパラメーター化の形式をサポートするようになりました(ただし、公式ドキュメントでこれに対する明確な参照は見つかりませんでした)。

# 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

私は、このフォームの意味がある内容を正確に把握していないが、それはそうですpytest.mark.parametrizeが、ことを認識test_tc1方法は、名前付き引数を取らないtester_argtesterそれが通じでパラメータ化引数を渡すので、それが使っていることを治具が行うtester治具。


同様の問題がありました-と呼ばれるフィクスチャがありtest_package、後で特定のテストでそれを実行するときに、そのフィクスチャにオプションの引数を渡せるようにしたかったのです。例えば:

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

(これらの目的のために、フィクスチャが何をするか、または返されるオブジェクトのタイプは重要ではありませんpackage)。

次に、このフィクスチャをテスト関数でversion使用して、そのテストで使用するフィクスチャへの引数も指定できるようにすることが望ましいでしょう。これは現時点では不可能ですが、優れた機能になる可能性があります。

その間、フィクスチャに、以前にフィクスチャが行ったすべての作業を実行する関数を返すだけで十分ですが、version引数を指定できます。

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

    return make_test_package

これで、次のようなテスト関数でこれを使用できます。

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

等々。

OPが試みた解決策は正しい方向に進んでおり、@ hpk42の回答が示唆MyTester.__init__しているように、はリクエストへの参照を次のように保存できます。

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"

次に、これを使用して次のようにフィクスチャを実装します。

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

必要に応じて、MyTesterクラスを少し再構成して、.args作成後に属性を更新し、個々のテストの動作を微調整することができます。


フィクスチャ内の関数のヒントをありがとう。私がこれに再び取り組むことができるまで少し時間がかかりましたが、これはかなり便利です!
マギー

2
このトピックに関する素晴らしい短い投稿:alysivji.github.io/pytest-fixures-with-function-arguments.html
maggie

「フィクスチャは直接呼び出すことを意図したものではなく、テスト関数がパラメータとして要求したときに自動的に作成されます。」というエラーは表示されませんか?
nz_21

153

これは実際には、py.testで間接的にパラメーター化されてネイティブにサポートされています。

あなたの場合、あなたは持っているでしょう:

@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

ああ、これはかなりいいです(あなたの例は少し古くなっていると思います-それは公式ドキュメントの例とは異なります)。これは比較的新しい機能ですか?これまでに会ったことはありません。これは問題の良い解決策でもあります-私の答えよりもいくつかの点で優れています。
イグアナナウ2016

2
このソリューションを使用してみましたが、複数のパラメーターを渡すか、リクエスト以外の変数名を使用すると問題が発生しました。@Iguananautのソリューションを使用してしまいました。
Victor Uriarte 2016年

42
これは受け入れられる答えになるはずです。公式ドキュメントのためのindirectキーワード引数は、おそらく、この基本的な手法のあいまいさを占めており、確かにまばらで非友好的です。私はこの機能のためにpy.testサイトを何度も精査しました-空で、古く、困惑しただけです。苦味は継続的インテグレーションとして知られている場所です。StackoverflowについてOdinに感謝します。
セシルカレー

1
このメソッドは、テストの名前を変更してパラメーターが含まれるようにすることに注意してください。test_tc1になりtest_tc1[tester0]ます。
jjj

1
それでindirect=True、呼び出されたすべてのフィクスチャにパラメーターを渡しますよね?そのためのドキュメントという名前のフィクスチャのため、明示的に名前の間接的なパラメータ化のための器具、例えばxindirect=['x']
winklerrr

11

フィクスチャ関数から(つまりTesterクラスから)要求モジュール/クラス/関数にアクセスできます。フィクスチャ関数からの要求テストコンテキストとの対話を参照してください。したがって、クラスまたはモジュールでいくつかのパラメーターを宣言すると、テスターフィクスチャがそれを取得できます。


3
私は次のようなことができることを知っています:(ドキュメントから)@ pytest.fixture(scope = "module"、params = ["merlinux.eu"、 "mail.python.org"])しかし、私はそれを行う必要がありますテストモジュール。フィクスチャーにパラメーターを動的に追加するにはどうすればよいですか?
マギー

2
重要なのは、フィクスチャ関数からのテストコンテキストの要求やり取りする必要はなく、フィクスチャ関数に引数を渡すための明確に定義された方法を持つことです。Fixture関数は、合意された名前の引数を受け取ることができるようにするためだけに、要求しているテストコンテキストのタイプを認識する必要はありません。たとえば、次のようにこれらの引数を記述@fixture def my_fixture(request)@pass_args(arg1=..., arg2=...) def test(my_fixture)て取得できるようにしたいとします。py.testでこのようなことは可能ですか?my_fixture()arg1 = request.arg1, arg2 = request.arg2
Piotr Dobrogost

7

ドキュメントは見つかりませんでしたが、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

これを指摘してくれてありがとう-これは最もクリーンな解決策のようです。以前のバージョンではこれが可能だったとは思いませんが、今は可能であることは明らかです。このフォームが公式ドキュメントのどこかに記載されているかどうか知っていますか?私はそれのような何かを見つけることができませんでしたが、それは明らかに機能します。この例を含めるために回答を更新しました。ありがとうございます。
イグアナナウ

1
github.com/pytest-dev/pytest/issues/5712と関連する(マージされた)PR を見ると、この機能では不可能だと思います。
ナデージュ

これは元に戻されましたgithub.com/pytest-dev/pytest/pull/6914
Maspe36

1
明確にするために、@ Maspe36はによってリンクされたPR Nadègeが元に戻されたことを示しています。したがって、この文書化されていない機能(まだ文書化されていないと思いますか?)はまだ存続しています。
blthayer

6

imiricの答えを少し改善するには、この問題を解決するもう1つのエレガントな方法は、「パラメータフィクスチャ」を作成することです。個人的にはのindirect機能よりも好きですpytest。この機能はから利用できpytest_cases、元のアイデアは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

は、フィクスチャでパラメーター化マークを使用できるようにし、別のモジュールの関数からパラメーターを取得できるようにすることにpytest-casesも注意し@pytest_fixture_plusてください@cases_data。詳細については、ドキュメントを参照してください。ちなみに私は作者です;)


1
これは今、単純なpytestでも機能するようです(v5.3.1があります)。つまり、なしでこれを機能させることができましたparam_fixtureこの回答を参照してください。私はそれのような例をドキュメントで見つけることができませんでした。これについて何か知っていますか?
イグアナナウト

情報とリンクをありがとう!これが実現可能であるとは思いもしませんでした。公式ドキュメントが彼らの意図を確認するのを待ちましょう。
smarie

2

私はこのようなフィクスチャを書くことができる面白いデコレータを作りました:

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

ここでは、左側に/他のフィクスチャーがあり、右側には次を使用して提供されるパラメーターがあります。

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

これは、関数の引数が機能するのと同じように機能します。age引数を指定しない場合69、代わりにデフォルトのが使用されます。指定しない場合name、またはdog.argumentsデコレータを省略した場合は、通常のものが得られますTypeError: dog() missing 1 required positional argument: 'name'。引数を取る別のフィクスチャがある場合name、それはこのフィクスチャと競合しません。

非同期フィクスチャもサポートされています。

さらに、これはあなたに素晴らしいセットアッププランを与えます:

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

完全な例:

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"

デコレーターのコード:

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
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.