【问题标题】:isinstance and Mockingisinstance 和 Mocking
【发布时间】:2012-06-21 21:03:42
【问题描述】:
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.为什么会抛出这个错误?他们是&lt;class type='MagicMock&gt;

Q2.如果错误得到修复,如何暂停模拟以便第一行通过?

来自docs

通常,对象的__class__ 属性将返回其类型。对于具有规范的模拟对象,__class__ 返回规范类。这允许模拟对象通过isinstance() 测试它们正在替换/伪装为的对象:

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

【问题讨论】:

  • 现在你知道为什么不鼓励使用isinstance了。
  • @MarkRansom 是的,它是邪恶的。但是确保我们传入的接口正确的最佳实践是什么? hasattr 似乎也没有解决这个问题。我认为两个对象可能具有相同的方法名称并使用错误的对象会使测试通过?我想问题的焦点已经转移了!啊。
  • 谢谢马克。我觉得话题转移了。但是错误的问题还没有解决:(当我想到一个关于 isinstance 的更具体的问题时,我会发一个新帖子。谢谢!
  • 您收到错误是因为HelloWorld(修补后)不是类或类型,而是mock.MagicMock 实例。正如错误所说,第二个参数必须是类、类型或类或类型的元组。您在 first 参数中引用我们的 spec 事物。这就是您在上一个示例中展示的内容(来自文档)。为什么,确切地说,您希望检查您的 HelloWorld 实例是否是模拟类型的实例(我认为这是不可能的)?
  • "Python 的许多优点之一是它允许“Duck Typing”,只要它执行您想要的操作,您就不必关心对象的确切类型" - 直到它没有。

标签: python unit-testing mocking


【解决方案1】:

恕我直言,这是一个很好的问题,并且说“不要使用isinstance,而是使用鸭子打字”是一个不好的答案。鸭子打字很棒,但不是灵丹妙药。有时isinstance 是必要的,即使它不是pythonic。例如,如果您使用一些非 Python 的库或遗留代码,您必须使用 isinstance。这只是真实的世界,mock 就是为了适应这种工作而设计的。

在代码中最大的错误是你写的时候:

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

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

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

这意味着当您修补HelloWorld 类对象时,对HelloWorld 的引用将被MagicMock 对象替换为test_mock() 函数的上下文。

然后,当i_call_hello_world()if isinstance(hw_obj, HelloWorld): 中执行时,HelloWorld 是一个MagicMock() 对象而不是一个类(如错误所示)。

这种行为是因为作为修补类引用的副作用,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 参数将其打扮成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

而只是单元测试部分的输出是

<class 'mock.MagicMock'>
here... check type: <type 'type'>
I am fake
None

【讨论】:

  • 此语句不正确:“HelloWorld 是一个 Mock() 对象而不是一个类(如错误提示)。”如果你捕捉到原始的 TypeError 并进行调试,你会看到执行 type(HelloWorld) 也会返回一个 .
  • @seanazlin 我稍后会检查它。但是为什么错误说 HelloWord 不是类或类型呢?无论如何感谢您的反馈。
  • @SeanAzlin 请在您的 Python 控制台上输入以下语句:type(MagicMock())type(MagicMock)type(object())type(object)。在那之后,我希望你会明白我写的是正确的。无论如何,如果您对我以前写Mock 而不是MagicMock 的评论,我认为这是错误的,但不是一个很大的问题......这只是我将修复的一个细节。使用 downvote 时要多加注意。我的答案是正确的,它是唯一一个不说就涵盖了原始问题的答案 *嘿,伙计!不要那样做”。
  • 我想我明白你的意思了。 type(HelloWorld) 返回 classobj 但 type(MagicMock(spec=HelloWorld)) 返回 mock.MagicMock。不过,之前在@mock.patch 的上下文中调试时,我看到type(HelloWorld) 返回&lt;class 'mock.MagicMock'&gt;,而不是mock.MagicMock。 OP看到了同样的事情。您所写的原始解决方案似乎暗示 &lt;class 'mock.MagicMock'&gt; 的 type(object) 的输出意味着当 OP 已经声明它不是时,该对象将被允许作为 isinstance() 的第二个参数。我还在误解你的解决方案吗?
  • Michele 我明白你的意思了。您的解决方案可以总结如下:1)不要尝试模拟HelloWorld类,然后将其作为第二个参数传递给isinstance。这无法正常工作,并且 2) 改为传入一个模拟对象(在示例中最终为 hw_obj ),使用 MagicMock(spec=HelloWorld) 创建,作为第一个参数isinstance .这将通过 isinstance 检查。如果这是要点,那么我认为您的解决方案很好,我很乐意 +1,但我建议强调 MagicMock(spec=HelloWorld) 对象需要作为第一个参数传递给 isinstance。跨度>
【解决方案2】:

在我看来,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)

【讨论】:

    【解决方案3】:

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

    【讨论】:

    • @NicHartley 只是覆盖 instancecheck 以及 subclasscheck。我扩展了我的答案。
    【解决方案4】:

    只需修补 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)
    

    【讨论】:

      【解决方案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')
      

      【讨论】:

      • 如果你需要模拟这个类,因为很多内部方法不能在测试上下文中使用?关于 Instance and Mocking 而不是 Mocking a method 的问题,Q1 和 Q2 的答案在哪里?我不会像您那样对您的答案投反对票,但也许您需要在提交答案之前更加注意问题。从技术角度来看您的回答为什么使用patch.object 您并不需要它。可以使用@patch('__main__.HelloWorld.say_it', return_value='I am fake'),更简洁易读。
      • @Micheled'Amico 好点。我提出的只是某些情况下的潜在解决方案(或者可能是一个更好的解决方案),例如当一个人正在对使用 isinstance() 的函数进行单元测试时,它调用了一个类的方法不要因为这个问题而被嘲笑。我认为解决方法可能比以前建议的解决方案更吸引一些人。在我的情况下它对我有用。
      • 当你有isinstance() 调用时你不能做的只是修补类,但你可以为对象使用模拟。顺便说一句:使用patch.object时要注意:仅在您真正需要它时使用它,或者很多时候您不明白为什么您的补丁不起作用。 (考虑从我的回答中删除反对票,因为您的观察是错误的)
      • 嘿....我知道如果你点击向上箭头,它不会变成 0 而是 1。无论如何,你必须做错事,你必须修复它.....不管它是什么
      【解决方案6】:

      我认为使用freezegun 是安全的。正确模拟datetime 模块的所有必要准备工作都在那里完成。此外,isinstance 检查对我来说也不会失败。

      它是这样工作的:

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

      【讨论】:

        【解决方案7】:

        isinstance 是一个内置函数,因此在this answer 中解释过,修补内置函数并不是一个好主意。为了让isinstance返回你想要的值,避免出现这个错误:

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

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

        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 作为上下文管理器,您可以只在要模拟它的特定函数/方法中进行模拟,而不是在整个测试中模拟它。

        【讨论】:

          【解决方案8】:

          我想可能的解决方案是检查对象的子类。

          issubclass(hw_obj.__class__, HelloWorld)
          

          例子:

          from unittest.mock import patch, MagicMock
          import unittest
          
          
          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) or issubclass(hw_obj.__class__, HelloWorld):
                  print(hw_obj.say_it())
          
          
          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)
          
          
          if __name__ == '__main__':
              unittest.main()
          

          【讨论】:

            【解决方案9】:

            不要使用isinstance,而是检查say_it 方法是否存在。如果方法存在,调用它:

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

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

            【讨论】:

            • 谢谢。但第一个问题实际上是为什么它会抛出那个错误?第二,采用这种变化,如果两个objs有相同的方法名,测试就通过了,对吧?
            • 我不知道为什么会抛出错误。没关系,很快你就不会使用isinstance :) 是的,现在你可以传入任何具有该方法并且行为“正确”的对象,并且测试将通过。
            • 不要使用isinstance() 对我来说不是一个解决方案:我尝试模拟 datetime.datetime 并在外部库中使用它。在我的情况下,django 使用它。
            • 投反对票,建议不要使用isinstance 是一个糟糕的答案。用 Mock 做正确的事,就像这样:stackoverflow.com/a/11283173/169153
            • 点赞。我来这个问题是因为我想知道如何测试isinstance。我认识到内德的回答没有解决 OP 的问题。但是,他的回答确实帮助我认识到使用try 而不是isinstance 我的代码会更好。
            猜你喜欢
            • 1970-01-01
            • 2010-12-31
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2020-04-13
            • 2019-01-30
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多