有没有办法将参数传递到pytest fixture中?

11

我并不是在谈论pytest中的Parameterizing a fixture功能,该功能允许对一个fixture进行硬编码的参数化,以便多次运行。

我的很多测试都遵循这样的模式:

httpcode = 401  # this is different per call
message = 'some message'  # this is different per call
url = 'some url'  # this is different per call


mock_req = mock.MagicMock(spec_set=urllib2.Request)
with mock.patch('package.module.urllib2.urlopen', autospec=True) as mock_urlopen, \
     mock.patch('package.module.urllib2.Request', autospec=True) as mock_request:
    mock_request.return_value = mock_req
    mock_urlopen.side_effect = urllib2.HTTPError(url, httpcode, message, {}, None)
    connection = MyClass()
    with pytest.raises(MyException):
        connection.some_function()  # this changes

本质上,我有一个类是API客户端,包括自定义的有意义的异常,将urllib2错误封装成特定于API的形式。因此,我有这样一个模式-修补一些方法,并在其中一个方法中设置副作用。我在可能有十几个不同的测试中使用它,唯一的区别是在side_effect的一部分中使用的三个变量以及我调用的MyClass()方法。

有没有办法将其作为pytest fixture,并传入这些变量?


如果核心代码在测试中仅通过方法名称有所不同,您可以只编写一个测试(使用getattr),并将方法名称(以及可能是任何调用参数的关键字字典,加上您自定义的异常类型)作为参数集中的附加组件传递。 - benjimin
5个回答

19

您可以使用间接的Fixture参数化:http://pytest.org/latest/example/parametrize.html#deferring-the-setup-of-parametrized-resources

@pytest.fixture()
def your_fixture(request):
    httpcode, message, url = request.param
    mock_req = mock.MagicMock(spec_set=urllib2.Request)
    with mock.patch('package.module.urllib2.urlopen', autospec=True) as mock_urlopen, \
         mock.patch('package.module.urllib2.Request', autospec=True) as mock_request:
        mock_request.return_value = mock_req
        mock_urlopen.side_effect = urllib2.HTTPError(url, httpcode, message, {}, None)
        connection = MyClass()
        with pytest.raises(MyException):
            connection.some_function()  # this changes


@pytest.mark.parametrize('your_fixture', [
    (403, 'some message', 'some url')
], indirect=True)
def test(your_fixture):
   ...

你的测试夹具将在带有所需参数的测试之前运行。


2
我明确地说过这不是我想要的... 我不希望重复创建 Fixture,我只想将参数传递给它。 - Jason Antman
2
在我的代码中,fixture 只在测试中运行一次。实际上,我的代码与你的代码相同。区别在于参数传递的方式。 如果你想在测试中生成参数,然后将它们传递给 fixture,那么你的代码是唯一的方法。如果参数在测试中预定义,那么我的代码也适用。 - Ilya Karpeev

6

在发帖后,我又进行了大量的研究,目前我能提供的最佳解决方案是:

使用 Fixture 无法达到你想要的效果。你可以尝试使用普通函数,如下所示:

def my_fixture(httpcode, message, url):
    mock_req = mock.MagicMock(spec_set=urllib2.Request)
    with mock.patch('package.module.urllib2.urlopen', autospec=True) as mock_urlopen, \
         mock.patch('package.module.urllib2.Request', autospec=True) as mock_request:
        mock_request.return_value = mock_req
        mock_urlopen.side_effect = urllib2.HTTPError(url, httpcode, message, {}, None)
        connection = MyClass()
        return (connection, mock_request, mock_urlopen)

def test_something():
    connection, mock_req, mock_urlopen = my_fixture(401, 'some message', 'some url')
    with pytest.raises(MyException):
        connection.some_function()  # this changes

我会使用@contextlib.contextmanager来实现你的“fixture”(而不是使用return等语句),这样Python就会在将环境(在此处执行mock的__exit__方法)传回测试之前尝试拆除/取消修补。 - benjimin

2
如何将参数传递到fixture中?
让我们分解一下这个想法:您正在请求一个fixture,它是一个可以响应参数的函数。因此,返回一个可以响应参数的函数即可:
@pytest.fixture
def get_named_service():
    def _get_named_service(name):
        result = do_something_with_name(name)
        return result
    return _get_named_service

因此,在测试中,您可以向函数提供参数:
def test_stuff(get_named_service):
    awesome_service = get_named_service('awesome')
    terrible_service = get_named_service('terrible')
    # Now you can do stuff with both services.

这是一个工厂模式的文档:
https://docs.pytest.org/en/latest/how-to/fixtures.html#factories-as-fixtures 正如原帖中所发现的,这只是一个函数,但它有一个优点,就是它在conftest中,所有其他常见的工具和设置/拆卸代码都驻留在那里;此外,它还能自我记录测试的依赖关系。

0

我知道这已经过时了,但也许对那些再次遇到这个问题的人有所帮助。

@pytest.fixture
def data_patcher(request):

    def get_output_test_data(filename, as_of_date=None):
         # a bunch of stuff to configure output
        return output

    def teardown():
        pass

    request.addfinalizer(teardown)

    return get_output_test_data

然后,在函数内部:

with patch('function to patch', new=data_patcher):

0
一些使用pytest.mark的技巧,我们可以创建带有参数的fixture。
from allure import attach
from pytest import fixture, mark


def get_precondition_params(request_fixture, fixture_function_name: str):
    precondition_params = request_fixture.keywords.get("preconditions_params")
    result = precondition_params.args[0].pop(fixture_function_name) if precondition_params is not None else None
    return result


@fixture(scope="function")
def setup_fixture_1(request):
    params = get_precondition_params(request, "setup_fixture_1")
    return params


@mark.preconditions_params(
    {
        "setup_fixture_1": {
            "param_1": "param_1 value for setup_fixture_1",
            "param_2": "param_2 value for setup_fixture_1"
        },
    }
)
def test_function(setup_fixture_1):
    attach(str(setup_fixture_1), "setup_fixture_1 value")

现在我们可以使用一个fixture代码,用标记参数化它,并在fixture内部执行任何操作。Fixture将作为前置条件执行(必须如此),而不是作为步骤执行(如果我们从fixture返回函数,则会这样)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接