Метод исправления только в одном модуле

#python #python-3.x #python-unittest #python-mock #python-unittest.mock

#python #python-3.x #python-unittest #python-макет #python-unittest.mock

Вопрос:

Например, у меня есть некоторый module( foo.py ) со следующим кодом:

 import requests

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

И модуль bar.py с аналогичным кодом:

 import requests

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

Я просто не могу понять, почему происходит следующее:

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

Они являются двумя издевательствами:

<MagicMock name='get().content' id='4352254472'>
<MagicMock name='get().content' id='4352254472'>

Кажется, что он исправляет только foo.get_ip метод из-за with patch('foo.requests.get') , но это не так.
Я знаю, что я могу просто вывести bar.get_fb вызов из области with видимости, но бывают случаи, когда я просто запускаю в context manager один метод, который вызывает много других, и я хочу исправить requests только в одном модуле.
Есть ли какой-нибудь способ решить эту проблему? Без изменения импорта в модуле

Комментарии:

1. Я думаю, что использование декоратора для исправления вашей функции должно сделать свое дело.

Ответ №1:

Два местоположения 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)
 

Итак, ваша проблема в том, что вы издеваетесь над объектом в модуле запроса, на который вы затем ссылаетесь как из 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())
 

Более прямым решением является изменение вашего кода таким образом, чтобы foo и bar извлекать ссылку 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. Насколько я могу судить, вы можете получить тот же эффект, сохранив foo.py и bar.py то же самое, и насмешливое foo.requests вместо foo.requests.get

2. Спасибо, но я также знаю это решение и упомянул об этом в сообщении Without changing imports in module . Я надеюсь, что есть решение, которое не требует изменения импорта скриптов

3. @elethan это работает, спасибо 😉 Но почему при издевательстве foo.requests он издевается только requests в foo.py , но при издевательстве foo.requests.get он издевается foo.py и bar.py . Можете ли вы написать полный ответ, почему это происходит?

4. @hasam Я думаю, это потому, что, когда вы издеваетесь foo.requests , он издевается над объектом модуля, который уже был импортирован foo , и поэтому не влияет на тот, который будет импортирован bar . Однако, когда вы имитируете foo.requests.get , он будет искать requests объект, импортированный в foo , затем искать get в исходном модуле и имитировать это, поэтому при bar импорте requests он получает издевательский get метод. Имеет ли это смысл?

5. @elethan да, спасибо. Я думаю, вам следует написать ответ на мой вопрос, и я приму его, потому что это было именно то, что мне было нужно

Ответ №2:

Не для того, чтобы украсть гром @Neapolitan, но другим вариантом было бы просто издеваться foo.requests вместо foo.requests.get :

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

Я думаю, что причина, по которой в вашем случае издеваются оба метода, заключается в том, что, поскольку он requests.get явно не импортирован foo.py , mock вам придется искать метод в requests модуле и издеваться над ним там, а не издеваться над ним в requests уже импортированном объекте foo , так что при bar последующем импорте requests и доступе requests.get к нему создается издевательствоверсия. Однако, если вы patch foo.requests вместо этого просто исправляете объект модуля, в который он уже импортирован foo , и исходный requests модуль не будет затронут.

Хотя эта статья не особенно полезна для этой конкретной проблемы, она очень полезна для понимания тонкостей patch

Комментарии:

1. есть ли какой-либо способ включить side_effect requests.get метод с таким подходом?

2. @hasam Да, пока вы import foo в своем тестовом модуле, вы должны быть в состоянии сделать foo.requests.get.side_effect = whatever_side_effect , так foo.requests.get как будет Mock объект.