Disable autouse fixtures on specific pytest marks
Asked Answered
D

7

79

Is it possible to prevent the execution of "function scoped" fixtures with autouse=True on specific marks only?

I have the following fixture set to autouse so that all outgoing requests are automatically mocked out:

@pytest.fixture(autouse=True)
def no_requests(monkeypatch):
    monkeypatch.setattr("requests.sessions.Session.request", MagicMock())

But I have a mark called endtoend that I use to define a series of tests that are allowed to make external requests for more robust end to end testing. I would like to inject no_requests in all tests (the vast majority), but not in tests like the following:

@pytest.mark.endtoend
def test_api_returns_ok():
    assert make_request().status_code == 200

Is this possible?

Disposure answered 3/8, 2016 at 15:47 Comment(0)
C
139

You can also use the request object in your fixture to check the markers used on the test, and don't do anything if a specific marker is set:

import pytest

@pytest.fixture(autouse=True)
def autofixt(request):
    if 'noautofixt' in request.keywords:
        return
    print("patching stuff")

def test1():
    pass

@pytest.mark.noautofixt
def test2():
    pass

Output with -vs:

x.py::test1 patching stuff
PASSED
x.py::test2 PASSED
Chagall answered 4/8, 2016 at 9:19 Comment(5)
Very useful! As an addendum... request.keywords["noautofixit"] only returns bool True when the mark is present. If your mark has args it's better to request.node.iter_markers("noautofixit") and then you get access to the Mark object with name, args and kwargs available.Cantillate
@Cantillate indeed - note that in most cases, you aren't interested in getting all markers if there are multiple, so request.node.get_closest_marker("name") might make things easier.Chagall
Fantastic: this opens up a whole new dimension of power provided by fixtures. Just one thing: I get "PytestUnknownMarkWarning: Unknown pytest.mark.noautofixt - is this a typo? You can register custom marks to avoid this warning - for details, see docs.pytest.org/en/stable/how-to/mark.html". You can edit the .ini file or use switch "--disable-warnings"... was this introduced in a relatively recent version of pytest?Deyoung
@mikerodent well, not relatively recent (since 2019: github.com/pytest-dev/pytest/issues/4826) but this post is some 6.5 years old.Chagall
@mikerodent noautofixt is no default marker, you can use that one or any other marker as you see fit your use case. To get rid of that warning, just add whatever marker you choose to use to your pytest.ini as documented here: docs.pytest.org/en/stable/how-to/mark.html#registering-marksDriblet
O
22

In case you have your endtoend tests in specific modules or classes you could also just override the no_requests fixture locally, for example assuming you group all your integration tests in a file called end_to_end.py:

# test_end_to_end.py

@pytest.fixture(autouse=True)
def no_requests():
    return

def test_api_returns_ok():
    # Should make a real request.
    assert make_request().status_code == 200

Ovular answered 7/8, 2020 at 13:54 Comment(1)
This is the solution for me -- just have a couple of tests that I don't want to use the session scoped fixture. All the other 100 or so tests are nicely on autouse. Also, the link to overriding in the pytest docs is gold.Bacteriostat
D
9

I wasn't able to find a way to disable fixtures with autouse=True, but I did find a way to revert the changes made in my no_requests fixture. monkeypatch has a method undo that reverts all patches made on the stack, so I was able to call it in my endtoend tests like so:

@pytest.mark.endtoend
def test_api_returns_ok(monkeypatch):
    monkeypatch.undo()
    assert make_request().status_code == 200
Disposure answered 3/8, 2016 at 17:39 Comment(0)
B
1

It would be difficult and probably not possible to cancel or change the autouse

You can't canel an autouse, being as it's autouse. Maybe you could do something to change the autouse fixture based on a mark's condition. But this would be hackish and difficult.

possibly with:

import pytest
from _pytest.mark import MarkInfo

I couldn't find a way to do this, but maybe the @pytest.fixture(autouse=True) could get the MarkInfo and if it came back 'endtoend' the fixture wouldn't set the attribute. But you would also have to set a condition in the fixture parameters.

i.e.: @pytest.fixture(True=MarkInfo, autouse=True). Something like that. But I couldn't find a way.


It's recommended that you organize tests to prevent this

You could just separate the no_requests from the endtoend tests by either:

  1. limit the scope of your autouse fixture
  2. put the no_requests into a class
  3. Not make it an auto use, just pass it into the params of each def you need it

Like so:

class NoRequests:
    @pytest.fixture(scope='module', autouse=True)
    def no_requests(monkeypatch):
        monkeypatch.setattr("requests.sessions.Session.request", MagicMock())
    def test_no_request1(self):
        # do stuff here
    # and so on

This is good practice. Maybe a different organization could help


But in your case, it's probably easiest to monkeypatch.undo()

Babel answered 3/8, 2016 at 21:52 Comment(0)
F
1

Just update the side effect to use the actual function.

import pytest
from unittest import mock

from my_module import my_func


@pytest.fixture(autouse=True)
def auto_use_mock():
    with mock.patch("my_module.my_func") as mocked_func:
        yield mocked_func


def test_skipping_autouse(auto_use_mock):
    # update the side effect to point at the thing you mocked
    auto_use_mock.side_effect = my_func
    
    ...rest of your test
Fibrinous answered 4/5, 2023 at 17:19 Comment(0)
C
0

pytest_django example - db fixture

from _pytest.fixtures import SubRequest

@pytest.fixture(autouse=True)
def _always_use_db(request: SubRequest):
    """To avoid having to use the db fixture in every test."""
    no_db = request.node.get_closest_marker("no_db") is not None
    if no_db:
        yield
    else:
        yield request.getfixturevalue("db")

usage:

def test_with_db():
    ...

@pytest.mark.no_db
def test_without_db():
    ...
Corroborate answered 29/11, 2023 at 16:10 Comment(0)
D
0

Just to go one step further, and building on the "noautofixt" technique of The Compiler, you can also potentially disable individual patches within a fixture using the technique below. Here I have a "configure_me" function at the start of a project which has a long list of tasks (subfunctions, checks on return values, etc.) it has to do. But how do I deliver by default a "successful configuration" using patching, but at the same time allow the possibility of using individual real (non-patched) functions when I want to?

@pytest.fixture()
def configuration_good(request):
    with mock.patch('start.main' if 'noautofixt_py_version_ok' in request.keywords else 'start.py_version_ok',  return_value=True):
        with mock.patch('start.cwd_is_prd',  return_value=True):
            with mock.patch('pathlib.Path.is_dir',  return_value=True):
                mock_module = mock.Mock()
                mock_module.logger = mock.Mock()
                mock_module.thread_check = mock.Mock()
                with mock.patch('start.try_to_get_library_main_module',  return_value=mock_module):
                    with mock.patch('start.try_to_get_lib_version',  return_value=packaging_version.Version('5.0')):
                        with mock.patch('start.try_to_get_min_lib_project_version',  return_value=packaging_version.Version('4.0')):
                            with mock.patch('start.other_instance_running',  return_value=False):
                                with mock.patch('start.try_to_configure_logger'):
                                    with mock.patch('start.try_to_get_app_version',  return_value=packaging_version.Version('1.0')):
                                        yield  

If the mark noautofixt_py_version_ok is detected, the if expression patches main instead of py_version_ok. I know that patching main for all the tests which will use this fixture will always be harmless. But a mock.patch must always patch something real: you can't say this, for example:

with mock.patch(None if 'noautofixt_py_version_ok' in request.keywords else 'start.py_version_ok',  return_value=True):
Deyoung answered 20/4 at 19:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.