isinstance和Mocking

时间:2012-06-21 21:03:43

标签: python unit-testing mocking

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。如果错误得到修复,如何暂停模拟以使第一行通过?

来自doc

  

通常,对象的属性将返回其类型。   对于具有spec 的模拟对象,将返回spec类   代替。这允许模拟对象通过isinstance测试   他们正在替换/伪装的对象:

mock = Mock(spec=3)
isinstance(mock, int)
True

由于

8 个答案:

答案 0 :(得分:38)

恕我直言,这是一个很好的问题并且说&#34; 不要使用isinstance,而是使用鸭子打字&#34;是一个糟糕的答案。鸭子打字很棒,但不是银弹。有时isinstance是必要的,即使它不是pythonic。例如,如果您使用的是某些不是pythonic的库或遗留代码,则必须使用isinstance。它只是现实世界,而mock是为了适应这种工作而设计的。

在代码中,最重要的错误是你写的时候:

@patch('__main__.HelloWorld', spec=HelloWorld)
def test_mock(self,MK):

patch documentation我们读到(强调是我的):

  

在函数体或with语句中,使用新的对象修补目标。

这意味着当您修补HelloWorld 类对象时,对HelloWorld的引用将被MagicMock对象替换为{{1}的上下文功能。

然后,在test_mock() i_call_hello_world()执行if isinstance(hw_obj, HelloWorld):时,HelloWorldMagicMock()对象,而不是类(如错误所示)。

这种行为是因为作为修补类引用的副作用,isinstance(hw_obj, HelloWorld)的第二个参数成为一个对象(MagicMock实例)。这不是classtype。理解此行为的简单实验是修改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参数将其设为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

答案 1 :(得分:6)

Michele d'Amico在我的观点中提供了correct answer,我强烈建议您阅读它。但是我花了一段时间才知道,并且,我相信将来我会回到这个问题,我认为一个最小的代码示例将有助于澄清解决方案并提供快速参考:

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)

答案 2 :(得分:2)

您可以通过从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

然后你可以将这个类与@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's mocked above using BaseMagicMock
        print("This is Class A")
    if issubclass(class_1, ClassB): # it's mocked above using BaseMagicMock
        print("This is Class B")
    if issubclass(class_1, ClassC): # it's 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)

答案 3 :(得分:0)

我在编写一些单元测试时最近一直在与自己搏斗。一个可能的解决方案是不实际尝试模拟整个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')

答案 4 :(得分:0)

我认为使用freezegun是安全的。在那里进行了正确模拟datetime模块的所有必要准备工作。另外,isinstance检查对我而言不会失败。

它是这样的:

@freeze_time("2019-05-15")
def test_that_requires_frozen_time(): ...

答案 5 :(得分:0)

只需用以下方法修补isinstance方法:

@patch('__main__.isinstance', return_value=True)

因此,您将获得预期的行为和覆盖范围,可以始终断言已调用了模拟方法,请参见下面的测试用例示例:

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 unittest.mock import patch, MagicMock
import unittest

class TestInstance(unittest.TestCase):
    @patch('__main__.isinstance', return_value=True)
    def test_mock(self,MK):
        print(type(MK))
        MK.say_it.return_value = 'I am fake'
        v = i_call_hello_world(MK)
        print(v)
        self.assertTrue(MK.say_it.called)

    @patch('__main__.isinstance', return_value=False)
    def test_not_call(self, MK):
        print(type(MK))
        MK.say_it.return_value = 'I am fake'
        v = i_call_hello_world(MK)
        print(v)
        self.assertFalse(MK.say_it.called)

答案 6 :(得分:0)

isinstance 是一个内置函数,如 this answer 中所述,修补内置函数并不是一个好主意。为了让 isinstance 返回你想要的值,并避免这个错误:

<块引用>

TypeError: isinstance() arg 2 必须是一个类型或类型的元组

您可以在被测模块中修补 isinstance。我还鼓励您在 patch 语句中使用 with 作为上下文管理器,如下所示:

from mock import patch


def test_your_test(self):
    # your test set up
    
    with patch('your_module.isinstance', return_value=True): # or False
        # logic that uses isinstance

使用 patch 作为上下文管理器允许您仅在要模拟的特定函数/方法中进行模拟,而不是在整个测试中进行模拟。

答案 7 :(得分:-6)

请勿使用isinstance,而是检查是否存在say_it方法。如果该方法存在,请将其命名为:

if hasattr(hw_obj, 'say_it'):
    print hw_obj.say_it()

无论如何,这是一个更好的设计:依赖类型信息会更加脆弱。