仅在一个模块中修补方法

has*_*sam 9 python python-3.x python-mock python-unittest python-unittest.mock

例如,我有一些foo.py带有以下代码的 module( ):

import requests

def get_ip():
    return requests.get('http://jsonip.com/').content
Run Code Online (Sandbox Code Playgroud)

bar.py具有类似代码的模块:

import requests

def get_fb():
    return requests.get('https://fb.com/').content
Run Code Online (Sandbox Code Playgroud)

我只是不明白为什么接下来会发生:

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())
Run Code Online (Sandbox Code Playgroud)

他们两个被嘲笑: <MagicMock name='get().content' id='4352254472'> <MagicMock name='get().content' id='4352254472'> 似乎只修补foo.get_ip方法with patch('foo.requests.get'),但事实并非如此。我知道我可能会bar.get_fb超出with范围进行调用,但在某些情况下,我只在上下文管理器中运行一种调用许多其他方法的方法,并且我只想requests在一个模块中进行修补。有什么办法可以解决这个问题吗?不改变模块中的导入

Nea*_*tan 5

这两个位置foo.requests.get引用bar.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)
Run Code Online (Sandbox Code Playgroud)

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


按照 @elethan 的建议,您可以模拟 foo 中的 requests 模块,甚至对 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())
Run Code Online (Sandbox Code Playgroud)

更直接的解决方案是改变您的代码,以便foobar引用get直接拉到它们的名称空间中。

foo.py:

from requests import get

def get_ip():
    return get('http://jsonip.com/').content
Run Code Online (Sandbox Code Playgroud)

酒吧.py:

from requests import get

def get_ip():
    return get('https://fb.com/').content
Run Code Online (Sandbox Code Playgroud)

主要.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())
Run Code Online (Sandbox Code Playgroud)

生产:

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

更新了更完整的解释,以及更好的解决方案(2016-10-15)

注意:添加了wraps=requests.get副作用后调用底层函数的功能。