Reputation: 4017
Suppose there are two packages in a project: some_package
and another_package
.
# some_package/foo.py:
def bar():
print('hello')
# another_package/function.py
from some_package.foo import bar
def call_bar():
# ... code ...
bar()
# ... code ...
I want to test another_package.function.call_bar
mocking out some_package.foo.bar
because it has some network I/O I want to avoid.
Here is a test:
# tests/test_bar.py
from another_package.function import call_bar
def test_bar(monkeypatch):
monkeypatch.setattr('some_package.foo.bar', lambda: print('patched'))
call_bar()
assert True
To my surprise it outputs hello
instead of patched
. I tried to debug this thing, putting an IPDB breakpoint in the test. When I manually import some_package.foo.bar
after the breakpoint and call bar()
I get patched
.
On my real project the situation is even more interesting. If I invoke pytest in the project root my function isn't patched, but when I specify tests/test_bar.py
as an argument - it works.
As far as I understand it has something to do with the from some_package.foo import bar
statement. If it's being executed before monkeypatching is happening then patching fails. But on the condensed test setup from the example above patching does not work in both cases.
And why does it work in IPDB REPL after hitting a breakpoint?
Upvotes: 89
Views: 45250
Reputation: 21
I also agree with Alex's answer: In this case you don't need to rewrite your code for testing.
For completeness, I want to point out that the import order makes a difference when patching.
Your example will work, if you import call_bar
after the monkeypatch:
# tests/test_bar.py
def test_bar(monkeypatch):
monkeypatch.setattr('some_package.foo.bar', lambda: print('patched'))
from another_package.function import call_bar
call_bar()
assert True
However, this gets tricky if you have multiple test cases with different patches, because modules are only imported once (even if you do from another_package.function import call_bar
again in another function).
You could also use importlib.reload
. In that case, it might make sense to do the monkeypatch in a context:
# tests/test_bar.py
import importlib
import sys
from another_package.function import call_bar
def test_bar(monkeypatch):
with monkeypatch.context() as mp:
mp.setattr('some_package.foo.bar', lambda: print('patched'))
# Reload the module with monkeypatch
importlib.reload(sys.modules['another_package.function'])
call_bar()
assert True
# Reload the module again without monkeypatch
importlib.reload(sys.modules['another_package.function'])
Upvotes: 1
Reputation: 1
I have a similar problem when I import, on module_b.py, the module to be mocked in a relative way. When I import using the full path of the module works perfectly.
Instead of from . import module_a
,
On module_b: from app import module_a
then, on test_module: from app.module_b import module_a
and monkeypatch(module_a, "func", mock_func)
Upvotes: 0
Reputation:
Named importation creates a new name for the object. If you then replace the old name for the object the new name is unaffected.
Import the module and use module.bar
instead. That will always use the current object.
import module
def func_under_test():
module.foo()
def test_func():
monkeypatch.setattr(...)
func_under_test
Upvotes: 30
Reputation: 4389
Another possible reason your function may not be getting patched is if your code is using multiprocessing.
On macOS the default start method for a new process has changed from fork
to spawn
. If spawn
is used, a brand new Python interpreter process is started, ignoring your recently patched function.
Fix: Set the default start method to fork
.
import multiprocessing
multiprocessing.set_start_method('fork', force=True)
You can add this snippet to conftest.py
inside your tests/
folder.
Upvotes: 2
Reputation: 1934
As Alex said, you shouldn't rewrite your code for your tests. The gotcha I ran into is which path to patch.
Given the code:
app/handlers/tasks.py
from auth.service import check_user
def handle_tasks_create(request):
check_user(request.get('user_id'))
create_task(request.body)
return {'status': 'success'}
Your first instinct to monkeypatch check_user
, like this:
monkeypatch.setattr('auth.service.check_user', lambda x: return None)
But what you want to do is patch the instance in tasks.py
. Likely this is what you want:
monkeypatch.setattr('app.handlers.tasks.check_user', lambda x: return None)
While the answers given are already good, I hope this brings more complete context.
Upvotes: 50
Reputation: 327
correct answer to OP's question:
monkeypatch.setattr('another_package.function.bar', lambda: print('patched'))
Upvotes: 2
Reputation: 19104
While Ronny's answer works it forces you to change application code. In general you should not do this for the sake of testing.
Instead you can explicitly patch the object in the second package. This is mentioned in the docs for the unittest module.
monkeypatch.setattr('another_package.bar', lambda: print('patched'))
Upvotes: 95