pytest: monkeypatch doesn't patch a module if it has already been imported by production code.

Thank you for this great testing library 😃

If I understand #762 correctly, modules should be reimported if pytest monkeypatch a module, like this:

monkeypatch.setattr('pytest_bug.b.foo', foo_patch)

even if the module has been imported this way:

from pytest_bug.b import foo

I have a reduced test case in this repo: https://github.com/JohanLorenzo/pytest-bug-2229. The README contains instructions about how to run it.

I checked the contexts via PDB:

  • When you’re in the test, foo is correctly patched.
  • However, when you’re in the function you’d like to test, foo hasn’t been changed.

Environment

  • Python 3.5.3 (On Arch Linux)
  • pip list: appdirs==1.4.0,packaging==16.8,py==1.4.32,pyparsing==2.1.10,pytest==3.0.6,pytest-bug==0.0.1,pytest-bug-2229==0.0.1,six==1.10.0

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 1
  • Comments: 25 (15 by maintainers)

Most upvoted comments

Instead, what is needed is a way for pytest to sandbox the import mechanism in tests

I think pytest-forked will do this for you, I actually use billiard.get_context("spawn") for this sort of thing in my projects.

That’s why a plugin should contain that difference in design that stems from from different design goals driven by different target audiences

@RonnyPfannschmidt unfortunately, I don’t think your advice would work for most mainstream, recommended, common workflows. For example it’s even one of the core principles of 12 Factor apps to store all config in the environment.

It’s extremely common to need to read os.environ and set module-level constants at import time. For example, it may be critical that the setting cannot change between function calls (thus os.environ shouldn’t be reconsulted on subsequent calls), and you may have logic of module instantiation that has to run at import time and is customized / affected by the state of the environment. Some use cases may have the luxury to replace this with function calls or maybe a clever use of an lru_cache, but it’s deeply unreasonable to say all use cases must refactor from first principles to do it that way.

This is especially common with third party dependencies that can’t be omitted or replaced, so the developer may have no control over relying on checks to os.environ happening at import time. The advice to longterm remove such dependencies would be literally intractable for the strong majority of users.

Instead, what is needed is a way for pytest to sandbox the import mechanism in tests, so that for the duration of the test, any uses of reload or manipulations of sys.modules cache doesn’t have a wider impact on any other test cases, parallel test executors, etc., and can be cleaned up / restored after the fact.

@nicoddemus I’d like to also add that the standard ‘patching in the wrong place’ advice doesn’t help if you need to patch a module-level variable specifically before the module is imported, because the setting of that variable will have a side-effect during the moment when the module itself is imported.

For example consider something like this:

#my_module.py
import os

if os.environ.get("MY_SETTING") is not None:
    important_variable = 'foo'
else:
    important_variable = 'bar'

def do_something():
    if important_variable == 'foo':
        return True
    return False

Suppose in my tests I want to monkeypatch the OS environment variable 'MY_SETTING' but before any tests will import my_module.py. I may even want to patch it multiple different ways and import my_module.py different ways with different patched variables that have different “start up” side effects.

Such as some conftest.py file similar to this

# conftest.py
import pytest


@pytest.fixture(scope='session', autouse=True)
def apply_my_setting_for_tests():
    from _pytest.monkeypatch import MonkeyPatch
    m = MonkeyPatch()
    m.setenv('MY_SETTING', 'BAZ')
    yield m
    m.undo()

But this does not appear to actually apply the patching, as though pytest collects and imports the module first, presenting no opportunity to get different on-import side effects.

@spearsem

short term: run the testrunner multiple times long term: try to completely remove the need for those dependencies as their global configuration is a liability

@spearsem loading of specific datasets in those cases is done by invoking functions, not by reloading modules after changing env vars

Hi @JohanLorenzo,

That’s a common gotcha with mocking in Python, and is not particular to monkeypatch but affects also other mocking modules including unittest.mock. I found an article that explains this common issue here: http://alexmarandon.com/articles/python_mock_gotchas, see the “Patching in the wrong place” section.

Closing this for now, but feel free to ask further questions!