9

For example, I have some module(foo.py) with next code:

import requests

def get_ip():
    return requests.get('http://jsonip.com/').content

And module bar.py with similiar code:

import requests

def get_fb():
    return requests.get('https://fb.com/').content

I just can't understand why next happens:

from mock import patch

from foo import get_ip
from bar import get_fb

with patch('foo.requests.get'):
    print(get_ip())
    print(get_fb())

They are two mocked: <MagicMock name='get().content' id='4352254472'> <MagicMock name='get().content' id='4352254472'> It is seemed to patch only foo.get_ip method due to with patch('foo.requests.get'), but it is not. I know that I can just get bar.get_fb calling out of with scope, but there are cases where I just run in context manager one method that calls many other, and I want to patch requests only in one module. Is there any way to solve this? Without changing imports in module

1
  • I think using a decorator to patch your function should do the trick. Commented Oct 15, 2016 at 21:00

2 Answers 2

5

The two locations foo.requests.get and bar.requests.get refer to the same object, so mock it in one place and you mock it in the other.

Imagine how you might implement patch. You have to find where the symbol is located and replace the symbol with the mock object. On exit from the with context you will need to restore the original value of the symbol. Something like (untested):

class patch(object):
    def __init__(self, symbol):
        # separate path to container from name being mocked
        parts = symbol.split('.')
        self.path = '.'.join(parts[:-1]
        self.name = parts[-1]
    def __enter__(self):
        self.container = ... lookup object referred to by self.path ...
        self.save = getattr(self.container, name)
        setattr(self.container, name, MagicMock())
    def __exit__(self):
        setattr(self.container, name, self.save)

So your problem is that the you are mocking the object in the request module, which you then are referring to from both foo and bar.


Following @elethan's suggestion, you could mock the requests module in foo, and even provide side effects on the get method:

from unittest import mock
import requests

from foo import get_ip
from bar import get_fb

def fake_get(*args, **kw):
    print("calling get with", args, kw)
    return mock.DEFAULT

replacement = mock.MagicMock(requests)
replacement.get = mock.Mock(requests.get, side_effect=fake_get, wraps=requests.get)
with mock.patch('foo.requests', new=replacement):
    print(get_ip())
    print(get_fb())

A more direct solution is to vary your code so that foo and bar pull the reference to get directly into their name space.

foo.py:

from requests import get

def get_ip():
    return get('http://jsonip.com/').content

bar.py:

from requests import get

def get_ip():
    return get('https://fb.com/').content

main.py:

from mock import patch

from foo import get_ip
from bar import get_fb

with patch('foo.get'):
    print(get_ip())
    print(get_fb())

producing:

<MagicMock name='get().content' id='4350500992'>
b'<!DOCTYPE html>\n<html lang="en" id="facebook" ...

Updated with a more complete explanation, and with the better solution (2016-10-15)

Note: added wraps=requests.get to call the underlying function after side effect.

Sign up to request clarification or add additional context in comments.

6 Comments

As far as I can tell, you can get the same effect by keeping foo.py and bar.py the same, and mocking foo.requests instead of foo.requests.get
Thanks, but I also know this solution and mentioned in post that Without changing imports in module. I hope there is a solution that does not require changing script imports
@elethan this works, thnx ;) But why when mocking foo.requests it mocks only requests in foo.py, but when mocking foo.requests.get, it mocks foo.py and bar.py. Can you write a full answer why it happens?
@hasam I think this is because when you mock foo.requests it is mocking the module object that has already been imported into foo, and so doesn't affect the one that will be imported in bar. However, when you mock foo.requests.get it will look up the requests object imported in foo, then look up get in the original module, and mock that, so when bar imports requests it is getting a mocked get method. Does that make sense?
@elethan yes, thanks. I think you should write an answer to my question and I will accept it, because that was exactly what I needed
|
1

Not to steal @Neapolitan's thunder, but another option would be to simply mock foo.requests instead of foo.requests.get:

with patch('foo.requests'):
    print(get_ip())
    print(get_fb())

I think the reason why both methods get mocked in your case is that, since requests.get is not explicitly imported in foo.py, mock will have to look up the method in the requests module and mock it there, rather than mocking it in the requests object already imported into foo, so that when bar later imports requests and accesses requests.get it is geting the mocked version. However, if you patch foo.requests instead, you are just patching the module object already imported into foo, and the original requests module will not be affected.

Although not particularly helpful for this particular problem, this article is very useful for understanding the subtleties of patch

2 Comments

is there any way to put a side_effect on requests.get method with this approach?
@hasam Yes, as long as you import foo in your test module, you should be able to do foo.requests.get.side_effect = whatever_side_effect, since foo.requests.get will be a Mock object.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.