#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
объект.