wong2
wong2

Reputation: 35760

Py.test patching a in-module object

I have a package:

- package/
  - __init__.py
  - cache.py
  - module1.py
  - module2.py
- tests/
  - test_module1.py
  - test_module2.py
  - conftest.py

both module1 and module2 are importing from cache.py:

from package.cache import cache

@cache()
def foo():
   ...

by default, cache is using file based caching provied by dogpile.cache, however when running tests, I'd like to mock cache with a memory based caching which is also supported by dogpile.cache.

here is what I'm tring:

# conftest.py

import pytest

@pytest.fixture(autouse=True)
def patch_cache(monkeypatch):
    from dogpile.cache import make_region
    m_cache = make_region().configure('dogpile.cache.memory')
    monkeypatch.setattr('package.cache.cache', m_cache)

as you can see, I created a fixture in which using monkeypatch to replace cache with m_cache

however, this doesn't work, when I run test with py.test, they're still using the old file based cache. is there anything wrong?

Upvotes: 3

Views: 1299

Answers (2)

erewok
erewok

Reputation: 7845

I had a similar problem recently with pytest and monkeypatching that I thought should have been resolved by using monkeypatch. I was trying to swap out the cache in my flask application for an in-memory one so that my cached views and other things didn't clobber real application cache keys accidentally.

The problem I discovered is that, like unittest.mock.patch, I had to monkeypatch the location where the patched thing is being imported and used, the call-site, in other words.

Imagine the following set of modules:

# package1/app1/module1.py

from flask_app import cache
cache.get("SOMEKEY")


# package1/app2/module1.py

from flask_app import cache
cache.get("SOMEKEY")


# package1/app2/module2.py

from package1.app2.module1 import cache
cache.get("SOMEKEY")

Now, in pytest in order to guarantee that all of these different versions of cache have been monkeypatched, I need a fixture that explicitly sets the attribute for all of them:

# conftest.py
from werkzeug.contrib.cache import SimpleCache

@pytest.fixture(scope="function", autouse=True)
def safe_cache(request, monkeypatch):
    """
    Monkeypatch the cache so it doesn't clobber real keys.

    Clear after every test.
    """
    cache = SimpleCache()
    monkeypatch.setattr('package1.app1.module1.cache', cache)
    monkeypatch.setattr('package1.app2.module1.cache', cache)
    monkeypatch.setattr('package1.app2.module2.cache', cache)    

    def teardown():
        cache.clear()
    request.addfinalizer(teardown)

    return cache

This is kind of annoying because every time I write a new module that imports the cache, I must also monkeypatch it in the fixture that monkeypatches these caches.

It works, however.

When I set a breakpoint in one of the modules importing the cache, and check what it's using, I see the following:

ipdb> cache
<werkzeug.contrib.cache.SimpleCache object at 0x10d658ac8>

My flask application is using a Redis cache otherwise, so seeing the above shows me it has been successful.

Upvotes: 0

Devin Jeanpierre
Devin Jeanpierre

Reputation: 95626

@cache() is applied when the module is imported, because the decorator is called at the top level of the module. If you monkeypatch it after that module is imported, your patched in version won't be applied.

Upvotes: 1

Related Questions