Pytest override fixture with parameterization - python

The answer to how to override a fixture with parametrization is already given here:
pytest test parameterization override
My question is what is the correct way to run the same test, both with the original fixture and the overridden values.
I'm already using the pytest-lazy-fixture library to override one fixture with another, but when I run the following pytest:
import pytest
from pytest_lazyfixture import lazy_fixture
#pytest.fixture
def fixture_value():
return 0
def test_foo(fixture_value):
...
#pytest.mark.parametrize('fixture_value', (lazy_fixture('fixture_value'), 1, 2, 3))
def test_bar(fixture_value):
...
I get this error:
E recursive dependency involving fixture 'fixture_value' detected

The error is because fixture_value name of the fixture is same as the paramter passed to the test.
import pytest
from pytest_lazyfixture import lazy_fixture
#pytest.fixture
def fixture_value():
return 0
def test_foo(fixture_value):
assert True
#pytest.mark.parametrize('fixtures_value'[(pytest.lazy_fixture('fixture_value'))]) # Square brackets used here.
def test_bar(fixtures_value): # Note the name here is changed.
assert True
Output:
Platform win32 -- Python 3.9.2, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
plugins: lazy-fixture-0.6.3
collected 2 items
myso_test.py::test_foo PASSED
myso_test.py::test_bar[fixtures_value0] PASSED

Related

Use Session Scoped Fixture as Variable

I am writing tests in pytest and am using fixtures as variables.
Originally, this is how the fixtures looked:
#pytest.fixture(scope="class")
def user(request):
u = "Matt"
request.cls.u = u
return u
And then, there was another fixture to delete the user from the database once I finished with it.
In the tests, I used both fixtures like so #pytest.mark.usefixtures("user", "teardown fixture")
The teardown fixture was class scoped, until I decided to change it to session scoped, since I want to delete the user only after running all the tests.
The problem was that suddenly the teardown fixture couldn't access user, since user is class scoped.
I changed the user to session scoped, however, I am not sure how to access, or export it now.
#pytest.fixture(scope="session")
def user(request):
u = "Matt"
# request.cls.user = u -> WHAT GOES HERE INSTEAD OF THIS?
return u
User is no longer recognized in the test functions. The test is located inside of a class. The current function is something like this:
Class TestUser(OtherClassWhichInheritsFromBaseCase):
def test_user1(self, user1):
self.open("www.google.com")
print(user1)
When I try to run the code in pycharm I get the following error:
def _callTestMethod(self, method):
> method()
E TypeError: TestUser.test_user1() missing 1 required positional argument: 'user1'
Any advice?
I think you're approaching this from the wrong direction. If you need to clean up a fixture, you don't write a second fixture; you write your fixture as a context manager.
For example, you might write:
#pytest.fixture(scope="session")
def user():
u = User(name="Matt")
yield u
# cleanup goes here
And in your test code:
def test_something(user):
assert user.name == "Matt"
Here's a complete example. We start with this dummy user.py, which simply creates files to demonstrate which methods were called:
from dataclasses import dataclass
#dataclass
class User:
name: str
def commit(self):
open("commit_was_called", "w")
def delete(self):
open("delete_was_called", "w")
Then here's our test:
import pytest
import user
#pytest.fixture(scope="session")
def a_user():
u = user.User(name="testuser")
u.commit()
yield u
u.delete()
class TestUserStuff:
def test_user(self, a_user):
assert a_user.name == "testuser"
We run it like this:
$ pytest
=============================================================================================== test session starts ===============================================================================================
platform linux -- Python 3.10.1, pytest-6.2.4, py-1.11.0, pluggy-0.13.1
rootdir: /home/lars/tmp/python
plugins: testinfra-6.5.0
collected 1 item
test_user.py . [100%]
================================================================================================ 1 passed in 0.00s ================================================================================================
After which we can confirm that both the commit and delete methods were called:
$ ls
commit_was_called
delete_was_called
test_user.py
user.py

how to override a pytest fixture, but still be able to access it?

I have a conftest.py and a plugin, both defining the same fixture with different implementations:
conftest.py
import pytest
#pytest.fixture
def f():
yield 1
plugin
import pytest
#pytest.fixture
def f():
yield 2
when installing the plugin, the conftest still overrides the plugin, so a test file will only see the conftest fixture, i.e.
test_.py
def test(f):
assert f == 1 # True
I want to be able to do something like this:
If the plugin is not installed, continue
Else, from the conftest plugin, yield the value of the plugin's fixture
I managed to get half of the way:
conftest.py
import pytest
#pytest.fixture
def f(pytestconfig):
if pytestconfig.pluginmanager.has_plugin(plugin_name):
# now what? I have get_plugin and import_plugin, but I'm not able to get the fixture from there...
The easiest way I see is to try and get the plugin fixture value. If the fixture lookup fails, then no plugin defined it and you can do your own thing. Example:
import pytest
from _pytest.fixtures import FixtureLookupError
#pytest.fixture
def f(request):
try: # try finding an already declared fixture with that name
yield request.getfixturevalue('f')
except FixtureLookupError:
# fixture not found, we are the only fixture named 'f'
yield 1

How to dynamically add new fixtures to a test based on the fixture signature of a test

So what I would like to achieve is mocking functions in various modules automatically with pytest. So I defined this in my conftest.py:
import sys
import __builtin__
from itertools import chain
# Fixture factory magic START
NORMAL_MOCKS = [
"logger", "error", "logging", "base_error", "partial"]
BUILTIN_MOCKS = ["exit"]
def _mock_factory(name, builtin):
def _mock(monkeypatch, request):
module = __builtin__ if builtin else request.node.module.MODULE
ret = Mock()
monkeypatch.setattr(module, name, ret)
return ret
return _mock
iterable = chain(
((el, False) for el in NORMAL_MOCKS),
((el, True) for el in BUILTIN_MOCKS))
for name, builtin in iterable:
fname = "mock_{name}".format(name=name)
_tmp_fn = pytest.fixture(name=fname)(_mock_factory(name, builtin))
_tmp_fn.__name__ = fname
setattr(
sys.modules[__name__],
"mock_{name}".format(name=name), _tmp_fn)
# Fixture normal factory magic END
This works and all, but I would like to omit the usage of the NORMAL_MOCKS and BUILTIN_MOCKS lists. So basically in a pytest hook I should be able to see that say there is a mock_foo fixture, but it's not registered yet, so I create a mock for it with the factory and register it. I just couldn't figure out how to do this. Basically I was looking into the pytest_runtest_setup function, but could not figure out how to do the actual fixture registration. So basically I would like to know with which hook/call can I register new fixture functions programatically from this hook.
One of the ways is to parameterize the tests at the collection/generation stage, i.e. before the test execution begins: https://docs.pytest.org/en/latest/example/parametrize.html
# conftest.py
import pytest
def mock_factory(name):
return name
def pytest_generate_tests(metafunc):
for name in metafunc.fixturenames:
if name.startswith('mock_'):
metafunc.parametrize(name, [mock_factory(name[5:])])
# test_me.py
def test_me(request, mock_it):
print(mock_it)
A very simple solution. But the downside is that the test is reported as parametrized when it actually is not:
$ pytest -s -v -ra
====== test session starts ======
test_me.py::test_me[it] PASSED
====== 1 passed in 0.01 seconds ======
To fully simulate the function args without the parametrization, you can make a less obvious trick:
# conftest.py
import pytest
def mock_factory(name):
return name
#pytest.hookimpl(hookwrapper=True)
def pytest_runtest_protocol(item, nextitem):
for name in item.fixturenames:
if name.startswith('mock_') and name not in item.funcargs:
item.funcargs[name] = mock_factory(name[5:])
yield
The pytest_runtest_setup hook is also a good place for this, as long as I've just tried.
Note that you do not register the fixture in that case. It is too late for the fixture registration, as all the fixtures are gathered and prepared much earlier at the collection/parametrization stages. In this stage, you can only execute the tests and provide the values. It is your responsibility to calculate the fixture values and to destroy them afterward.
The snippet below is a pragmatic solution to "how to dynamically add fixtures".
Disclaimer: I don't have expertise on pytest. I'm not saying this is what pytest was designed for, I just looked at the source code and came up with this and it seems to work. The fact that I use "private" attributes means it might not work with all versions (currently I'm on pytest 7.1.3)
from _pytest.fixtures import FixtureDef
from _pytest.fixtures import SubRequest
import pytest
#pytest.fixture(autouse=True) # autouse is relevant, as then the fixture registration happens in-time. It's too late if requiring the fixture without autouse e.g. like `#pytest.mark.usefixtures("add_fixture_dynamically")`
def add_fixture_dynamically(request: SubRequest):
"""
Conditionally and dynamically adds another fixture. It's conditional on the presence of:
#pytest.mark.my_mark()
"""
marker = request.node.get_closest_marker("my_mark")
# don't register fixture if marker is not present:
if marker is None:
return
def your_fixture(): # the name of the fixture must match the parameter name, like other fixtures
return "hello"
# register the fixture just-in-time
request._fixturemanager._arg2fixturedefs[your_fixture.__name__] = [
FixtureDef(
argname=your_fixture.__name__,
func=your_fixture,
scope="function",
fixturemanager=request._fixturemanager,
baseid=None,
params=None,
),
]
yield # runs the test. Could be wrapped in try/except/finally
# suppress warning (works if this and `add_fixture_dynamically` are in `conftest.py`)
def pytest_configure(config):
"""Prevents printing of the warning 'PytestUnknownMarkWarning: Unknown pytest.mark.<fixture_name>'"""
config.addinivalue_line("markers", "my_mark")
#pytest.mark.my_mark()
def test_adding_fixture_dynamically(your_fixture):
assert your_fixture == "hello"

Fixture scope doesn't work when parametrized tests use parametrized fixtures

I want to share fixtures between different instantiations of the same parametrized tests, where the fixtures themselves are also parametrized:
#!/usr/bin/py.test -sv
import pytest
numbers_for_fixture = [0]
def pytest_generate_tests(metafunc):
if "config_field" in metafunc.fixturenames:
metafunc.parametrize("config_field", [1], scope='session')
#pytest.fixture(scope = 'session')
def fixture_1(config_field):
numbers_for_fixture[0] += 1
return '\tfixture_1(%s)' % numbers_for_fixture[0]
#pytest.fixture(scope = 'session')
def fixture_2():
numbers_for_fixture[0] += 1
return '\tfixture_2(%s)' % numbers_for_fixture[0]
def test_a(fixture_1):
print('\ttest_a:', fixture_1)
def test_b(fixture_1):
print('\ttest_b:', fixture_1)
#pytest.mark.parametrize('i', range(3))
def test_c(fixture_1, i):
print('\ttest_c[%s]:' % i, fixture_1)
#pytest.mark.parametrize('i', range(3))
def test_d(fixture_2, i):
print('\ttest_d[%s]:' % i, fixture_2)
I get this output:
platform linux -- Python 3.4.1 -- py-1.4.26 -- pytest-2.6.4 -- /usr/bin/python
collecting ... collected 8 items
test.py::test_a[1] test_a: fixture_1(1)
PASSED
test.py::test_b[1] test_b: fixture_1(1)
PASSED
test.py::test_c[1-0] test_c[0]: fixture_1(1)
PASSED
test.py::test_c[1-1] test_c[1]: fixture_1(2)
PASSED
test.py::test_c[1-2] test_c[2]: fixture_1(3)
PASSED
test.py::test_d[0] test_d[0]: fixture_2(4)
PASSED
test.py::test_d[1] test_d[1]: fixture_2(4)
PASSED
test.py::test_d[2] test_d[2]: fixture_2(4)
PASSED
test_a, test_b and test_c[0] all share fixture_1(1). All the test_ds share fixture_2(4). The problem is that the test_cs use different versions of fixture_1.
This also happens when scopes are set to "module" and "class", and it only happens when both the test and the fixture are parametrized.
From the way pytest prints the test parametesr, it seems like it doesn't distinguish between the different types of parameters used for each item, so it creates a fixture for each set of parameters rather then for each unique subset of a parameters list that the fixture uses.
Is this a bug in pytest, or did I neglect to set some configuration or something? Is there a workaround?
In a nut shell, there's a bug where the scoping is effectively ignored in this case. Run pytest with the --setup-show switch to get a better view of when the fixtures are set up and torn down.
Workaround
Comment out the pytest_generate_tests function and add the following:
#pytest.fixture('session', [1])
def config_field(request):
return request.param
Now you should see the correct set up and tear down nesting.
Workaround for dynamic parameters
I needed to set my parameters from a cli agument and had to use pytest_generate_tests
Set indirect to True and paramters to the cli argument:
def pytest_generate_tests(metafunc):
if "config_field" in metafunc.fixturenames:
metafunc.parametrize(
"config_field",
metafunc.config.getoption('some_option'),
True,
'session'
)
Add a place holder fixture:
#pytest.fixture('session')
def config_field(request):
return request.param
Well, You can return yield from your feature, this might return the same value:
#pytest.fixture(scope = 'session')
def fixture_1(config_field):
.. yield <fixture-properties>

How to call setup once for all tests and teardown after all are finished

I have a bunch of tests written using pytest. There are all under a directory dir. For example:
dir/test_base.py
dir/test_something.py
dir/test_something2.py
...
The simplified version of code in them is as follows:
test_base.py
import pytest
class TestBase:
def setup_module(module):
assert False
def teardown_module(module):
assert False
test_something.py
import pytest
from test_base import TestBase
class TestSomething(TestBase):
def test_dummy():
pass
test_something2.py
import pytest
from test_base import TestBase
class TestSomethingElse(TestBase):
def test_dummy2():
pass
All my test_something*.py files extend the base class in test_base.py. Now I wrote setup_module(module) and teardown_module(module) methods in test_base.py. I was expecting the setup_module to be called once for all tests, and teardown_module() to be called at the end, once all tests are finished.
But the functions don’t seem to be getting called? Do I need some decorators for this to work?
The OP's requirement was for setup and teardown each to execute only once, not one time per module. This can be accomplished with a combination of a conftest.py file, #pytest.fixture(scope="session") and passing the fixture name to each test function.
These are described in the Pytest fixtures documentation
Here's an example:
conftest.py
import pytest
#pytest.fixture(scope="session")
def my_setup(request):
print '\nDoing setup'
def fin():
print ("\nDoing teardown")
request.addfinalizer(fin)
test_something.py
def test_dummy(my_setup):
print '\ntest_dummy'
test_something2.py
def test_dummy2(my_setup):
print '\ntest_dummy2'
def test_dummy3(my_setup):
print '\ntest_dummy3'
The output when you run py.test -s:
collected 3 items
test_something.py
Doing setup
test_dummy
.
test_something2.py
test_dummy2
.
test_dummy3
.
Doing teardown
The name conftest.py matters: you can't give this file a different name and expect Pytest to find it as a source of fixtures.
Setting scope="session" is important. Otherwise setup and teardown will be repeated for each test module.
If you'd prefer not to pass the fixture name my_setup as an argument to each test function, you can place test functions inside a class and apply the pytest.mark.usefixtures decorator to the class.
Put setup_module and teardown_module outside of a class on module level. Then add your class with your tests.
def setup_module(module):
"""..."""
def teardown_module(module):
"""..."""
class TestSomething:
def test_dummy(self):
"""do some tests"""
For more info refer to this article.
setup_module/teardown_module are called for the module where the eventual (derived) tests are defined. This also allows to customize the setup. If you only ever have one setup_module you can put it to test_base.py and import it from the other places. HTH.
First of all it is a good practice to put all tests in a module called "tests":
<product>
...
tests/
__init__.py
test_something.py
Secondly I think you should use setup_class/teardown_class methods in your base class:
import unittest
class MyBase(unittest.TestCase):
#classmethod
def setup_class(cls):
...
#classmethod
def teardown_class(cls):
...
More info: http://pytest.org/latest/xunit_setup.html

Categories