Patch method only in one module

时间:2016-10-15 17:25:53

标签: python python-3.x python-unittest python-mock python-unittest.mock

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

2 个答案:

答案 0 :(得分:2)

两个位置foo.requests.getbar.requests.get引用同一个对象,因此在一个地方嘲笑它,然后在另一个地方嘲笑它。

想象一下如何实现补丁。您必须找到符号所在的位置,并用模拟对象替换该符号。退出with上下文时,您需要恢复符号的原始值。像(未经测试)的东西:

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)

所以你的问题是你在模拟请求模块中的对象,然后你从foo和bar中引用它。

按照@ elethan的建议,您可以在foo中模拟请求模块,甚至可以为get方法提供副作用:

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())

更直接的解决方案是改变您的代码,以便foobar将对get的引用直接拉入其名称空间。

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())
制造

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

更新了更完整的解释,并提供了更好的解决方案(2016-10-15)

注意:添加wraps=requests.get以在副作用后调用基础函数。

答案 1 :(得分:1)

不要窃取 @Neapolitan 的雷声,但另一种选择就是简单地模仿foo.requests而不是foo.requests.get

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

我认为两种方法在您的案例中被嘲笑的原因是,由于requests.get未明确导入foo.pymock必须在requests中查找方法{1}}模块并在那里进行模拟,而不是在已导入requests的{​​{1}}对象中进行模拟,以便foo稍后导入bar并访问{{ 1}}它正在制作模拟版本。但是,如果您requests requests.get,则只需修补已导入patch的模块对象,原始foo.requests模块不会受到影响。

虽然对此特定问题没有特别帮助,this article对于理解foo

的细微之处非常有用