From 05f3422d7ca2ea75b5a5a1565a64642c847f6782 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 20 Jul 2016 22:05:49 -0300 Subject: [PATCH] Make monkeypatch invocation-scoped --- _pytest/monkeypatch.py | 9 ++++++--- doc/en/invocation-fixture.rst | 3 --- doc/en/monkeypatch.rst | 37 +++++++++++++++++++++-------------- testing/test_monkeypatch.py | 34 +++++++++++++++++++++++++++++++- 4 files changed, 61 insertions(+), 22 deletions(-) diff --git a/_pytest/monkeypatch.py b/_pytest/monkeypatch.py index d1de4a679..258de385f 100644 --- a/_pytest/monkeypatch.py +++ b/_pytest/monkeypatch.py @@ -10,7 +10,7 @@ import pytest RE_IMPORT_ERROR_NAME = re.compile("^No module named (.*)$") -@pytest.fixture +@pytest.fixture(scope='invocation') def monkeypatch(request): """The returned ``monkeypatch`` fixture provides these helper methods to modify objects, dictionaries or os.environ:: @@ -25,9 +25,11 @@ def monkeypatch(request): monkeypatch.chdir(path) All modifications will be undone after the requesting - test function has finished. The ``raising`` + test function or fixture has finished. The ``raising`` parameter determines if a KeyError or AttributeError will be raised if the set/deletion operation has no target. + + This fixture is ``invocation``-scoped. """ mpatch = MonkeyPatch() request.addfinalizer(mpatch.undo) @@ -97,7 +99,8 @@ notset = Notset() class MonkeyPatch: - """ Object keeping a record of setattr/item/env/syspath changes. """ + """ Object returned by the ``monkeypatch`` fixture keeping a record of setattr/item/env/syspath changes. + """ def __init__(self): self._setattr = [] diff --git a/doc/en/invocation-fixture.rst b/doc/en/invocation-fixture.rst index 4e49323c4..80f00adf7 100644 --- a/doc/en/invocation-fixture.rst +++ b/doc/en/invocation-fixture.rst @@ -9,9 +9,6 @@ Invocation-scoped fixtures This feature is experimental, so if decided that it brings too much problems or considered too complicated it might be removed in pytest ``3.1``. - ``tmpdir`` and ``monkeypatch`` might become ``invocation`` scoped - fixtures in the future if decided to keep invocation-scoped fixtures. - Fixtures can be defined with ``invocation`` scope, meaning that the fixture can be requested by fixtures from any scope, but when they do they assume the same scope as the fixture requesting it. diff --git a/doc/en/monkeypatch.rst b/doc/en/monkeypatch.rst index 4155a3a34..cc6b94958 100644 --- a/doc/en/monkeypatch.rst +++ b/doc/en/monkeypatch.rst @@ -6,7 +6,7 @@ Monkeypatching/mocking modules and environments Sometimes tests need to invoke functionality which depends on global settings or which invokes code which cannot be easily -tested such as network access. The ``monkeypatch`` function argument +tested such as network access. The ``monkeypatch`` fixture helps you to safely set/delete an attribute, dictionary item or environment variable or to modify ``sys.path`` for importing. See the `monkeypatch blog post`_ for some introduction material @@ -14,6 +14,9 @@ and a discussion of its motivation. .. _`monkeypatch blog post`: http://tetamap.wordpress.com/2009/03/03/monkeypatching-in-unit-tests-done-right/ +As of pytest-3.0, the ``monkeypatch`` fixture is :ref:`invocation-scoped ` +meaning it can be requested from fixtures of any scope. + Simple example: monkeypatching functions --------------------------------------------------- @@ -53,27 +56,31 @@ This autouse fixture will be executed for each test function and it will delete the method ``request.session.Session.request`` so that any attempts within tests to create http requests will fail. -example: setting an attribute on some class ------------------------------------------------------- +example: setting an environment variable for the test session +------------------------------------------------------------- -If you need to patch out ``os.getcwd()`` to return an artificial -value:: +If you would like for an environment variable to be +configured for the entire test session, you can add this to your +top-level ``conftest.py`` file: - def test_some_interaction(monkeypatch): - monkeypatch.setattr("os.getcwd", lambda: "/") +.. code-block:: python -which is equivalent to the long form:: + # content of conftest.py + @pytest.fixture(scope='session', autouse=True) + def enable_debugging(monkeypatch): + monkeypatch.setenv("DEBUGGING_VERBOSITY", "4") - def test_some_interaction(monkeypatch): - import os - monkeypatch.setattr(os, "getcwd", lambda: "/") - +This auto-use fixture will set the ``DEBUGGING_VERBOSITY`` environment variable for +the entire test session. + +Note that the ability to use a ``monkeypatch`` fixture from a ``session``-scoped +fixture was added in pytest-3.0. -Method reference of the monkeypatch function argument ------------------------------------------------------ +Method reference of the monkeypatch fixture +------------------------------------------- -.. autoclass:: monkeypatch +.. autoclass:: MonkeyPatch :members: setattr, replace, delattr, setitem, delitem, setenv, delenv, syspath_prepend, chdir, undo ``monkeypatch.setattr/delattr/delitem/delenv()`` all diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index 7599be47f..0c152a8b6 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -328,4 +328,36 @@ def test_issue1338_name_resolving(): try: monkeypatch.delattr('requests.sessions.Session.request') finally: - monkeypatch.undo() \ No newline at end of file + monkeypatch.undo() + + +def test_invocation_scoped_monkeypatch(testdir): + testdir.makeconftest(""" + import pytest + import sys + + @pytest.fixture(scope='module') + def stamp_sys(monkeypatch): + monkeypatch.setattr(sys, 'module_stamped', True, raising=False) + """) + testdir.makepyfile(test_inv_mokeypatch_1=""" + import sys + + def test_stamp_1(monkeypatch, stamp_sys): + assert sys.module_stamped + monkeypatch.setattr(sys, 'function_stamped', True, raising=False) + assert sys.function_stamped + + def test_stamp_2(monkeypatch): + assert sys.module_stamped + assert not hasattr(sys, 'function_stamped') + """) + testdir.makepyfile(test_inv_mokeypatch_2=""" + import sys + + def test_no_stamps(): + assert not hasattr(sys, 'module_stamped') + assert not hasattr(sys, 'function_stamped') + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines(['*3 passed*'])