Исинстанция и издевательство
class HelloWorld(object):
def say_it(self):
return 'Hello I am Hello World'
def i_call_hello_world(hw_obj):
print 'here... check type: %s' %type(HelloWorld)
if isinstance(hw_obj, HelloWorld):
print hw_obj.say_it()
from mock import patch, MagicMock
import unittest
class TestInstance(unittest.TestCase):
@patch('__main__.HelloWorld', spec=HelloWorld)
def test_mock(self,MK):
print type(MK)
MK.say_it.return_value = 'I am fake'
v = i_call_hello_world(MK)
print v
if __name__ == '__main__':
c = HelloWorld()
i_call_hello_world(c)
print isinstance(c, HelloWorld)
unittest.main()
Вот обратная связь
here... check type: <type 'type'>
Hello I am Hello World
True
<class 'mock.MagicMock'>
here... check type: <class 'mock.MagicMock'>
E
======================================================================
ERROR: test_mock (__main__.TestInstance)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/usr/local/lib/python2.7/dist-packages/mock.py", line 1224, in patched
return func(*args, **keywargs)
File "t.py", line 18, in test_mock
v = i_call_hello_world(MK)
File "t.py", line 7, in i_call_hello_world
if isinstance(hw_obj, HelloWorld):
TypeError: isinstance() arg 2 must be a class, type, or tuple of classes and types
----------------------------------------------------------------------
Ran 1 test in 0.002s
Q1. Почему выдается эта ошибка? Они являются <class type='MagicMock>
Q2. Как мне приостановить насмешку, чтобы первая строка прошла, если ошибка исправлена?
Из документов:
Обычно атрибут __class__
объекта возвращает его тип. Для фиктивного объекта со спецификацией __class__
вместо этого возвращает класс спецификации. Это позволяет фиктивным объектам проходить isinstance()
для объекта, который они заменяют/маскируют как:
mock = Mock(spec=3)
isinstance(mock, int)
True
Ответы
Ответ 1
Не используйте isinstance
, а вместо этого проверяйте существование метода say_it
. Если метод существует, назовите его:
if hasattr(hw_obj, 'say_it'):
print hw_obj.say_it()
В любом случае это лучший дизайн: использование информации о типе гораздо более хрупкое.
Ответ 2
ИМХО, это хороший вопрос, а высказывание "не используйте isinstance
, вместо этого используйте duck typing" - плохой ответ. Утка набирает отлично, но не серебряная пуля. Иногда isinstance
необходим, даже если это не pythonic. Например, если вы работаете с какой-то библиотекой или устаревшим кодом, который не является пифоническим, вы должны поиграть с isinstance
. Это просто реальный мир, и макет был разработан, чтобы соответствовать такой работе.
В коде большая ошибка, когда вы пишете:
@patch('__main__.HelloWorld', spec=HelloWorld)
def test_mock(self,MK):
Из документации patch
мы читаем (подчеркну мое):
Внутри тела функции или с оператором цель исправляется новым объектом.
Это означает, что когда вы исправляете объект класса HelloWorld
ссылка на HelloWorld
будет заменена объектом MagicMock
для контекста функции test_mock()
.
Затем, когда i_call_hello_world()
выполняется в if isinstance(hw_obj, HelloWorld):
HelloWorld
- это MagicMock()
а не класс (как предполагает ошибка).
Такое поведение объясняется тем, что в качестве побочного эффекта исправления ссылки на класс 2-й аргумент isinstance(hw_obj, HelloWorld)
становится объектом (экземпляр MagicMock
). Это не class
или type
. Простой эксперимент, чтобы понять это поведение - изменить i_call_hello_world()
следующим образом:
HelloWorld_cache = HelloWorld
def i_call_hello_world(hw_obj):
print 'here... check type: %s' %type(HelloWorld_cache)
if isinstance(hw_obj, HelloWorld_cache):
print hw_obj.say_it()
Ошибка исчезнет, поскольку исходная ссылка на класс HelloWorld
сохраняется в HelloWorld_cache
при загрузке модуля. Когда патч будет применен, он изменится только HelloWorld
а не HelloWorld_cache
.
К сожалению, предыдущий эксперимент не дает нам никакого способа поиграть с подобными вам случаями, потому что вы не можете изменить библиотеку или устаревший код, чтобы ввести такой трюк. Более того, это те уловки, которые мы бы не хотели видеть в нашем коде.
Хорошая новость заключается в том, что вы можете что-то сделать, но вы не можете просто patch
ссылку HelloWord
в модуле, где у вас есть isinstace(o,HelloWord)
код для тестирования. Лучший способ зависит от реального случая, который вы должны решить. В вашем примере вы можете просто создать Mock
для использования в качестве объекта HelloWorld
, использовать аргумент spec
чтобы isinstance
его в экземпляр HelloWorld
и пройти тест isinstance
. Это как раз одна из целей, для которой разработана spec
. Ваш тест будет написан так:
def test_mock(self):
MK = MagicMock(spec=HelloWorld) #The hw_obj passed to i_call_hello_world
print type(MK)
MK.say_it.return_value = 'I am fake'
v = i_call_hello_world(MK)
print v
И вывод только unittest часть
<class 'mock.MagicMock'>
here... check type: <type 'type'>
I am fake
None
Ответ 3
Michele d'Amico предоставляет правильный ответ на мой взгляд, и я настоятельно рекомендую его прочитать. Но мне потребовалось некоторое время, и, как я уверен, я вернусь к этому вопросу в будущем, я подумал, что минимальный пример кода поможет прояснить решение и дать краткую ссылку:
from mock import patch, mock
class Foo(object): pass
# Cache the Foo class so it will be available for isinstance assert.
FooCache = Foo
with patch('__main__.Foo', spec=Foo):
foo = Foo()
assert isinstance(foo, FooCache)
assert isinstance(foo, mock.mock.NonCallableMagicMock)
# This will cause error from question:
# TypeError: isinstance() arg 2 must be a class, type, or tuple of classes and types
assert isinstance(foo, Foo)
Ответ 4
Вы можете сделать это, унаследовавшись от класса MagicMock
и переопределив метод __subclasscheck__
:
class BaseMagicMock(MagicMock):
def __subclasscheck__(self, subclass):
# I couldn't find another way to get the IDs
self_id = re.search("id='(.+?)'", self.__repr__()).group(1)
subclass_id = re.search("id='(.+?)'", subclass.__repr__()).group(1)
return self_id == subclass_id
# def __instancecheck__(self, instance) for 'isinstance'
И тогда вы можете использовать этот класс с декоратором @patch
:
class FooBarTestCase(TestCase):
...
@patch('app.services.ClassB', new_callable=BaseMagicMock)
@patch('app.services.ClassA', new_callable=BaseMagicMock)
def test_mock_for_issubclass_support(self, ClassAMock, ClassBMock):
check_for_subclasses(ClassAMock)
Это!
Примечания:
Вы ДОЛЖНЫ высмеивать все классы, которые сравниваются с использованием issubclass
.
Пример:
def check_for_subclasses(class_1):
if issubclass(class_1, ClassA): # it mocked above using BaseMagicMock
print("This is Class A")
if issubclass(class_1, ClassB): # it mocked above using BaseMagicMock
print("This is Class B")
if issubclass(class_1, ClassC): # it not mocked with @patch
print("This is Class C")
issubclass(class_1, ClassC)
приведет к ошибке {TypeError}issubclass() arg 1 must be a class
, потому что ClassC
содержит по умолчанию __issubclass__
метод. И тогда мы должны обработать тест следующим образом:
class FooBarTestCase(TestCase):
...
@patch('app.services.ClassC', new_callable=BaseMagicMock)
@patch('app.services.ClassB', new_callable=BaseMagicMock)
@patch('app.services.ClassA', new_callable=BaseMagicMock)
def test_mock_for_issubclass_support(self, ClassAMock, ClassBMock):
check_for_subclasses(ClassAMock)
Ответ 5
Я занимался этим сам в последнее время, когда писал некоторые модульные тесты. Одно из потенциальных решений - фактически не пытаться издеваться над всем классом HelloWorld, а вместо этого выкрикивать методы класса, которые вызывается тестируемым кодом. Например, что-то вроде этого должно работать:
class HelloWorld(object):
def say_it(self):
return 'Hello I am Hello World'
def i_call_hello_world(hw_obj):
if isinstance(hw_obj, HelloWorld):
return hw_obj.say_it()
from mock import patch, MagicMock
import unittest
class TestInstance(unittest.TestCase):
@patch.object(HelloWorld, 'say_it')
def test_mock(self, mocked_say_it):
mocked_say_it.return_value = 'I am fake'
v = i_call_hello_world(HelloWorld())
self.assertEquals(v, 'I am fake')