Overriding sub-fixtures in pytest - python

I'm using pytest with some complicated dependency-injected fixtures. I have fixtures that use other fixtures in a long chain. I'd like to be able to alter some fixtures in the middle of the chain for specific tests.
Given these (simplified) fixtures:
#pytest.fixture
def cache():
return Cache()
# Use cache fixture in a new fixture.
#pytest.fixture
def resource(cache):
return Resource(cache=cache, working=True)
# Use resource fixture in a new fixture.
#pytest.fixture
def service(resource):
return Service(resource=resource)
And some tests:
def test_service_when_resource_working(service):
assert service.status == "good"
def test_service_when_resource_broken(service):
assert service.status == "bad"
How can I override the resource fixture so that it's like this:
#pytest.fixture
def broken_resource(cache):
return Resource(cache=cache, working=False)
...but only for the test_service_when_resource_broken test case? I can create a broken_service that uses broken_resource, but the reality is that the dependency chain is long, and I want to re-use all the fixtures, but selectively change some of them in the middle for selected tests.
I want to do something like this (pseudocode):
#pytest.override_fixture('resource', 'broken_resource')
def test_service_when_resource_broken(service):
# service should have been instantiated with broken_resource instead of resource.
assert service.status == "bad"

You can use markers on your tests to achieve what you are expecting.
Basically, you mark the test for which you need a different behaviour. In the fixture method look for that marker from the requesting test context and process.
Here is how you can do it.
#pytest.fixture
def cache():
return Cache()
# Use cache fixture in a new fixture.
#pytest.fixture
def resource(request, cache):
working = True
marker = request.node.get_marker("broken")
if marker:
working = False
return Resource(cache=cache, working=working)
# Use resource fixture in a new fixture.
#pytest.fixture
def service(resource):
return Service(resource=resource)
def test_service_when_resource_working(service):
assert service.status == "good"
#pytest.mark.broken
def test_service_when_resource_broken(service):
assert service.status == "bad"

Related

How to get caller name inside pytest fixture?

Assume we have:
#pytest.fixture()
def setup():
print('All set up!')
return True
def foo(setup):
print('I am using a fixture to set things up')
setup_done=setup
I'm looking for a way to get to know caller function name (in this case: foo) from within setup fixture.
So far I have tried:
import inspect
#pytest.fixture()
def setup():
daddy_function_name = inspect.stack()[1][3]
print(daddy_function_name)
print('All set up!')
return True
But what gets printed is: call_fixture_func
How do I get foo from printing daddy_function_name?
You can use the built-in request fixture in your own fixture:
The request fixture is a special fixture providing information of the requesting test function.
Its node attribute is the
Underlying collection node (depends on current request scope).
import pytest
#pytest.fixture()
def setup(request):
return request.node.name
def test_foo(setup):
assert setup == "test_foo"

How to pass test status to it's teardown, through a fixture preferably

I have a BaseTest class which has tear_down and I want to have inside tear_down a variable representing wether or not the test has failed.
I tried look at A LOT of older posts but I coulden't implement them as they were hooks or mixture of hook and fixture and something did not work on my end.
What is the best practice for doing that?
Last thing I've tried was -
#pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item):
outcome = yield
rep = outcome.get_result()
# set a report attribute for each phase of a call, which can
# be "setup", "call", "teardown"
setattr(item, "rep_" + rep.when, rep)
Then pass request fixture to teardown and inside use
has_failed = request.node.rep_call.failed
But request had no attributes at all, it was a method.
Also tried -
#pytest.fixture
def has_failed(request):
yield
return True if request.node.rep_call.failed else False
and pass it like that.
def teardown_method(self, has_failed):
And again, no attributes.
Isn't there a simple fixture to just do like request.test_status or something like that?
It's important that the teardown will have that bool parameter wether or not it failed and not do stuff outside the teardown.
Thanks!
There doesn't appear to be any super simple fixture offering the test report as a fixture. And I see what you mean: most examples of recording the test report are geared toward non-unittest use cases (including the official docs). However, we can adjust these examples to work with unittest TestCases.
There appears to be a private _testcase attribute on the item arg passed to pytest_runtest_makereport, which contains the instance of the TestCase. We can set an attribute on it, which can then be accessed within teardown_method.
# conftest.py
import pytest
#pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
if report.when == 'call' and hasattr(item, '_testcase'):
item._testcase.did_pass = report.passed
And here's a dinky little example TestCase
import unittest
class DescribeIt(unittest.TestCase):
def setup_method(self, method):
self.did_pass = None
def teardown_method(self, method):
print('\nself.did_pass =', self.did_pass)
def test_it_works(self):
assert True
def test_it_doesnt_work(self):
assert False
When we run it, we find it prints the proper test failure/success bool
$ py.test --no-header --no-summary -qs
============================= test session starts =============================
collected 2 items
tests/tests.py::DescribeIt::test_it_doesnt_work FAILED
self.did_pass = False
tests/tests.py::DescribeIt::test_it_works PASSED
self.did_pass = True
========================= 1 failed, 1 passed in 0.02s =========================

Shorter option names for pytest fixture variants with long names

I am testing a function with several incoming datasets defined as fixtures, but the fixture names get quite cumbersome to distinguish them from one another.
#pytest.fixture()
def dataset_with_foo():
pass
#pytest.fixture()
def dataset_with_bar():
pass
#pytest.fixture()
def dataset_with_foo_and_bar():
pass
def test_something(dataset_with_foo_and_bar):
pass
Is there a way to define some kind of alias for the option name to be shorter? For instance, something like:
#usesfixture("dataset_with_foo_and_bar", option_name="dataset")
def test_something(dataset):
pass
Create a super fixture and helper function to get desired fixture with one fixture.
import pytest
#pytest.fixture
def super_fixture(fixture1,fixture2,fixture3):
local_vars = locals()
def helper(fixture_name):
return local_vars.get(fixture_name)
return helper
def test_a(super_fixture):
# getting fixture1
assert super_fixture("fixture1")
Ok, the best way I could find to do it is by using deferred parametrized fixtures:
#pytest.fixture()
def dataset(request):
mapping = {
"with-foo": create_dataset_with_foo(),
"with-bar": create_dataset_with_bar(),
"with-foo-and-bar": create_dataset_with_foo_and_bar(),
}
return mapping[request.param]
def create_dataset_with_foo():
pass
def create_dataset_with_bar():
pass
def create_dataset_with_foo_and_bar():
pass
#pytest.mark.parametrize("dataset", ["with-foo"], indirect=True)
def test_something(dataset):
pass
#pytest.mark.parametrize("dataset", ["with-foo-and-bar"], indirect=True)
def test_something(dataset):
pass
There has been other attempts using pytest-lazy-fixture or specialized decorator, but I find it a bit too hacky..
https://gist.github.com/wcooley/7472b8de6edb1e8ceda560843c0519a8

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"

How to share object from fixture to all tests using pytest?

What is the best way to define an object in a fixture with session scope and autouse=True, so it will be available to all tests?
#pytest.fixture(scope='session', autouse=True)
def setup_func(request):
obj = SomeObj()
Next thing, I want some magic that previously created obj will appear in each test context without the need of each test to define the setup_func fixture.
def test_one():
obj.do_something_fancy()
My recommendation would to add the fixture to conftest.py and make sure to return the object you want to produce from the fixture.
As noted, this makes "autouse" kind of useless.
In the root directory for your tests, add the fixture to a file named conftest.py:
#pytest.fixture(scope='session', autouse=True)
def someobj(request):
return SomeObj()
Any test file beneath the root file will have access to this fixture (for example test_foo.py):
def test_foo(someobj):
assert isinstance(someobj, SomeObj)
Another approach, would be to use a global variable defined in the same test or imported from a module.
For example in conftest.py:
someobj = None
#pytest.fixture(scope='session', autouse=True)
def prep_someobj(request):
someobj = SomeObj()
Then in your test:
from . import conftest
def test_foo():
assert isinstance(conftest.someobj, SomeObj)
In my opinion this is less readable and more cumbersome than the first method.
A more general pattern for this is to return locals() at the end of your conftest and you'll be able to easily reference anything created in the fixture.
conftest.py
#pytest.fixture(scope='session')
def setup_func(request):
obj1 = SomeObj()
obj2 = SomeObj()
return locals()
test_stuff.py
def test_one(setup_func):
setup_func['obj1'].do_something_fancy()
def test_two(setup_func):
setup_func['obj2'].do_something_fancy()
Another possibility is to wrap your tests in a class and use class variables to only define the object instance once. This assumes you are able to wrap all tests in a single class and so this answer may address a less general, but similar use case. For example,
class SomeObj():
"""This object definition may exist in another module and be imported."""
def __init__(self):
self.x = 5
def do_something_fancy(self, y):
return self.x * y
class TestX():
# Object instance to share across tests
someobj = SomeObj()
def test_x(self):
assert TestX.someobj.x == 5
def test_fancy(self):
fancy_factor = 10
result = TestX.someobj.do_something_fancy(fancy_factor)
assert result == 50

Categories