Исинстанция и издевательство

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