Skip to content Skip to sidebar Skip to footer

Isinstance And 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

Solution 1:

IMHO this is a good question and saying "don't use isinstance, use duck typing instead" is a bad answer. Duck typing is great, but not a silver bullet. Sometimes isinstance is necessary, even if it is not pythonic. For instance, if you work with some library or legacy code that isn't pythonic you must play with isinstance. It is just the real world and mock was designed to fit this kind of work.

In the code the big mistake is when you write:

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

From patch documentation we read (emphasize is mine):

Inside the body of the function or with statement, the target is patched with a new object.

That means when you patch the HelloWorldclass object the reference to HelloWorld will be replaced by a MagicMock object for the context of the test_mock() function.

Then, when i_call_hello_world() is executed in if isinstance(hw_obj, HelloWorld):HelloWorld is a MagicMock() object and not a class (as the error suggests).

That behavior is because as a side effect of patching a class reference the 2nd argument of isinstance(hw_obj, HelloWorld) becomes an object (a MagicMock instance). This is neither a class or a type. A simple experiment to understand this behavior is to modify i_call_hello_world() as follows:

HelloWorld_cache = HelloWorld

defi_call_hello_world(hw_obj):
    print'here... check type: %s' %type(HelloWorld_cache)
    ifisinstance(hw_obj, HelloWorld_cache):
        print hw_obj.say_it()

The error will disappear because the original reference to HelloWorld class is saved in HelloWorld_cache when you load the module. When the patch is applied it will change just HelloWorld and not HelloWorld_cache.

Unfortunately, the previous experiment doesn't give us any way to play with cases like yours because you cannot change the library or legacy code to introduce a trick like this. Moreover, these are that kind of tricks that we would like to never see in our code.

The good news is that you can do something ,but you cannot just patch the HelloWord reference in the module where you have isinstace(o,HelloWord) code to test. The best way depends on the real case that you must solve. In your example you can just create a Mock to use as HelloWorld object, use spec argument to dress it as HelloWorld instance and pass the isinstance test. This is exactly one of the aims for which spec is designed. Your test would be written like this:

deftest_mock(self):
    MK = MagicMock(spec=HelloWorld) #The hw_obj passed to i_call_hello_worldprinttype(MK)
    MK.say_it.return_value = 'I am fake'
    v = i_call_hello_world(MK)
    print v

And the output of just unittest part is

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

Solution 2:

Michele d'Amico provides the correct answer in my view and I strongly recommend reading it. But it took me a while a grok and, as I'm sure I'll be coming back to this question in the future, I thought a minimal code example would help clarify the solution and provide a quick reference:

from mock import patch, mock

classFoo(object): pass# Cache the Foo class so it will be available for isinstance assert.
FooCache = Foo

with patch('__main__.Foo', spec=Foo):
    foo = Foo()
    assertisinstance(foo, FooCache)
    assertisinstance(foo, mock.mock.NonCallableMagicMock)

    # This will cause error from question:# TypeError: isinstance() arg 2 must be a class, type, or tuple of classes and typesassertisinstance(foo, Foo)

Solution 3:

You can do it by being inherited from the MagicMock class and overriding the __subclasscheck__ method:

classBaseMagicMock(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`

And then you can use this class with the @patch decorator:

classFooBarTestCase(TestCase):
    ...

    @patch('app.services.ClassB', new_callable=BaseMagicMock)    @patch('app.services.ClassA', new_callable=BaseMagicMock)deftest_mock_for_issubclass_support(self, ClassAMock, ClassBMock):
        check_for_subclasses(ClassAMock)

That's it!




Remarks:

You MUST mock all classes which are compared using issubclass.

Example:

defcheck_for_subclasses(class_1):
    ifissubclass(class_1, ClassA): # it's mocked above using BaseMagicMockprint("This is Class A")
    ifissubclass(class_1, ClassB): # it's mocked above using BaseMagicMockprint("This is Class B")
    ifissubclass(class_1, ClassC): # it's not mocked with @patchprint("This is Class C")

issubclass(class_1, ClassC) will cause an error {TypeError}issubclass() arg 1 must be a class because ClassC contains a default __issubclass__ method. And then we should handle the test like this:

classFooBarTestCase(TestCase):
    ...

    @patch('app.services.ClassC', new_callable=BaseMagicMock)    @patch('app.services.ClassB', new_callable=BaseMagicMock)    @patch('app.services.ClassA', new_callable=BaseMagicMock)deftest_mock_for_issubclass_support(self, ClassAMock, ClassBMock):
        check_for_subclasses(ClassAMock)

Solution 4:

Just patch the isinstance method with:

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

So you will get expected behavior and coverage, you can always assert that mock method was called, see test case sample below:

classHelloWorld(object):
    defsay_it(self):
        return'Hello I am Hello World'defi_call_hello_world(hw_obj):
    print('here... check type: %s' %type(HelloWorld))
    ifisinstance(hw_obj, HelloWorld):
        print(hw_obj.say_it())

from unittest.mock import patch, MagicMock
import unittest

classTestInstance(unittest.TestCase):
    @patch('__main__.isinstance', return_value=True)deftest_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)deftest_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)

Solution 5:

I've been wrestling with this myself lately while writing some unit tests. One potential solution is to not actually try to mock out the entire HelloWorld class, but instead mock out the methods of the class that are called by the code you are testing. For example, something like this should work:

classHelloWorld(object):
    defsay_it(self):
        return'Hello I am Hello World'defi_call_hello_world(hw_obj):
    ifisinstance(hw_obj, HelloWorld):
        return hw_obj.say_it()

from mock import patch, MagicMock
import unittest

classTestInstance(unittest.TestCase):
    @patch.object(HelloWorld, 'say_it')deftest_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')

Post a Comment for "Isinstance And Mocking"