isinstance and Mocking
Asked Answered
T

12

51
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 is the traceback

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. Why is this error thrown? They are <class type='MagicMock>

Q2. How do I pause the mocking so that the first line will pass if the error is fixed?

From the docs:

Normally the __class__ attribute of an object will return its type. For a mock object with a spec, __class__ returns the spec class instead. This allows mock objects to pass isinstance() tests for the object they are replacing / masquerading as:

mock = Mock(spec=3)
isinstance(mock, int)
True
Tendency answered 21/6, 2012 at 21:3 Comment(9)
Now you know why the use of isinstance is discouraged.Skiing
@MarkRansom Yes it is evil. But what is the best practice to ensure the interface we pass in is CORRECT? hasattr doesn't seem to solve the gap either. Two objects may have same method names and uses the wrong object will make the test pass, I think? I guess the question's focus has shifted! Ahh.Tendency
That's the point - one of the many nice things about Python is that it allows "Duck Typing" where you don't care about the exact type of an object as long as it does what you want. You might need to take some care in method naming to make sure you don't define the same name with two different meanings, but it brings great flexibility to the code in the end. It's a feature, not a bug.Skiing
Thanks Mark. I think the topic is shifted. But the problem with the error hasn't solved yet :( I will make a new post when I think of a more concrete question with respect to isinstance. Thanks!Tendency
You're getting the error because HelloWorld (after patching) isn't a class or a type, but a mock.MagicMock instance. As the error says, the second argument must be a class, type, or tuple of classes or types. The spec thing you refer to us for the first argument. That's what you're showing in your last example (from the docs). Why, exactly, do you wish to check whether your HelloWorld instance is an instance of an emulated type (which is, I think, impossible)?Broom
"one of the many nice things about Python is that it allows "Duck Typing" where you don't care about the exact type of an object as long as it does what you want" - until it doesn't.Entertainer
In my case, I needed to mock datetime.datetime because I want to 'now' returns a specific date. But I have some libs that check if some value is datetime.datetime and I don't know how can I solve this. I need the inverse: "if instance(some_value, datetime.datetime)" but datetime.datetime is my mock. spec do not resolve. Can somebody help me?Dolhenty
@CppLearner, I have added an answer in which I propose an alternative of mocking isinstance in the particular module that it is used.Ronnieronny
mock (MagicMock) object can be used as the first arg, but can not be used as the second arg in isinstance() check because __instancecheck__ is officially unsupported magic method (by design). But mock.NonCallableMagicMock can be used as the second arg (see the answer below by klenwell).Drily
C
-7

Don't use isinstance, instead check for the existence of the say_it method. If the method exists, call it:

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

This is a better design anyway: relying on type information is much more brittle.

Caralie answered 21/6, 2012 at 21:9 Comment(10)
Thanks. But the first question would really be why is it throwing that error? And two, adopting this change, if two objs have the same method name, the test will pass, right?Tendency
I don't know why the error is thrown. It doesn't matter, soon you won't be using isinstance :) And yes, now you can pass in any object that has the method, and behaves "properly", and the test will pass.Caralie
Thanks. I will stop using isinstance after reading the major fallback. BUt still... if MyMomKitchenObject has say_it and programmer uses that as the input of the function... the test will still PASS, doesn't it? So how should I verify my unittest actually works? or how should I determine its correctness in my code? Just like integration test, two objs can have 99% same interface, and the system under test never uses that 1% different, and the test still passes, the system will "work without problem".Tendency
Usually, an unittest is not about checking that a method exists, but checking whether it returns what you expect or not.Skilled
Thanks Thomas. But that's not what I was asking. Unittest does the coverage, and using mock it can help us to isolate dependencies from the system under test. But we want to know what happen if someone pass in class X rather than class Y. Is it a good practice to check that in the code? I don't know... I will think for a while and then make a new post on this subject.Tendency
@Tendency Yes, I understood that. What I meant is that Python usually encourages Duck Typing, that is, if your object behaves like you expect it to behave, then it's OK. So, your unittests should focus on the behavior. If someone passes a class that doesn't behave like it should, then their unittests should fail.Skilled
Don't use isinstance() is not a solution for me: I try to mock datetime.datetime and it is used in external libraries. In my case django uses it.Gooseberry
@Tendency I post a answer that fit exactly your question.Roman
Downvote, suggesting to not use isinstance is a terrible answer. Do it right with Mock, much like this: https://mcmap.net/q/354866/-can-i-fake-mock-the-type-of-my-mock-objects-in-python-unittestsDiandiana
Upvote. I came to this question because I wanted to know how to test isinstance. I recognize that Ned's answer does not address the OP's question. However, his answer did help me recognize that my code would be better with try instead of isinstance.Alarice
S
77

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)
def test_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 HelloWorld class 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

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

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:

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

And the output of just unittest part is

<class 'mock.MagicMock'>
here... check type: <type 'type'>
I am fake
None
Subterrane answered 25/10, 2014 at 22:8 Comment(9)
This statement is incorrect: "HelloWorld is a Mock() object and not a class (as the error suggested)." If you catch the original TypeError and debug, you'll see that executing type(HelloWorld) will also return a <class 'mock.MagicMock'>.Telespectroscope
@seanazlin I'll check it later. But why the error say that HelloWord is not a class or type? Anyway thanks for your feedback.Roman
@SeanAzlin Please type on your python console follow statements: type(MagicMock()), type(MagicMock), type(object()), type(object). After that I hope you'll understand that what I had wrote is correct. Anyway if your comment about I used to write Mock instead of MagicMock, I think that is wrong but not a very big issue ... It is just a detail that I will fix. Pay more attention when you use downvote. My answer is correct and it is the only one that cover the original question without say *Hey guy! Don't do that".Roman
I think I see your point. type(HelloWorld) returns classobj but type(MagicMock(spec=HelloWorld)) returns mock.MagicMock. When previously debugging in the context of @mock.patch, though, I was seeing type(HelloWorld) return <class 'mock.MagicMock'>, not mock.MagicMock. The OP was seeing the same thing. Your original solution as-written seems to imply that an output from type(object) of <class 'mock.MagicMock'> means that the object would be allowed as the 2nd arg of isinstance() when the OP already stated that it isn't. Am I still misunderstanding your solution?Telespectroscope
@SeanAzlin What I mean is that the output of type(MagicMock()) (so the type of a MegicMock object instance) is exactly <class 'mock.MagicMock'>. Anyway, even it seams a class it is not a class but an object when you try to use it in isinstance. This ambiguity is removed in python 3 where isinstace take just type or tuple of types. In python class and object concepts are really close... When you use patch you replace the original reference by a new object (MagicMock as default) event if it is a class... class are just functions that return an object.Roman
@SeanAzlin My original solution say that if in some place you have something like isinstance(o,MyClass) you cannot patch MyClass reference of isinstance call anymore. That means you need to find some other ways like use mock object directly.Roman
@SeanAzlin If you need other clarifications please ask... otherwise remove the downvote.Roman
Michele I see your point now. Can your solution be summarized as follows: 1) Don't try to mock the HelloWorld class and then pass that as the 2nd arg to isinstance. That can't be made to work, and 2) instead, pass in a mocked object (ultimately hw_obj in the example), made with MagicMock(spec=HelloWorld), as the 1st arg to isinstance. That will pass the isinstance check. If that's the gist then I think your solution is a fine one and I'll happily +1 but I would recommend emphasizing that the MagicMock(spec=HelloWorld) object needs to be passed in to isinstance as the 1st arg.Telespectroscope
spec= doesn't seem to be working for me. I'm setting the spec on the appropriate mock; the spec is correct; but isinstance is still throwing errors.Levana
P
18

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

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)
Partizan answered 6/1, 2016 at 22:20 Comment(1)
Great! Now I applied mocker.patch(typecheck_class, mock.NonCallableMagicMock) to all I need.Drily
B
5

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

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`

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

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)

That's it!




Remarks:

You MUST mock all classes which are compared using issubclass.

Example:

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

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)
Blob answered 24/4, 2018 at 14:5 Comment(1)
@NicHartley just override instancecheck along with subclasscheck. I extended my answer.Blob
R
2

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:

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)
Rotogravure answered 7/10, 2020 at 11:36 Comment(0)
R
2

isinstance is a built-in function, and it is not a good idea to patch built-in functions as it is explained in this answer. In order to make isinstance return the value you want, and avoid this error:

TypeError: isinstance() arg 2 must be a type or tuple of types

You can patch isinstance in the module under test. I also encourage you to use patch as a context manager in a with statement as follows:

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

Using patch as a context manager allows you to mock just in the specific function/method that you want to mock it instead of mocking it in the whole test.

Ronnieronny answered 21/5, 2021 at 4:32 Comment(0)
T
0

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:

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')
Telespectroscope answered 4/2, 2015 at 23:3 Comment(4)
What about if you need mock the class because lot of internal methods cannot used in testing context? The question speaking about Instance and Mocking and not Mocking a method and where are the answers to Q1 and Q2 ? I don't downvote your answer like you did but maybe you need to pay more attention to questions before file answers. About your answer from a technical point of view why use patch.object you don't really need it. You can use @patch('__main__.HelloWorld.say_it', return_value='I am fake') that is more concise and simpler to read.Roman
@Micheled'Amico Fine points. What I'm proposing is only a potential solution (or maybe work-around is a better term) for some situations, such as when one is unit testing a function that uses isinstance() and that calls the method of a class that can't be mocked because of this issue. I think that work-around may be more appealing to some than previously suggested solutions. It worked for me in my situation.Telespectroscope
What you cannot do when you have isinstance() call is just patch the class, but you can use a mock for the object. BTW: pay attention when you use patch.object: use it only when you really need it or lot of time you will not understand why your patch doesn't work. (Consider to remove the downvote from my answer because your observation are wrong)Roman
Hey.... I know that if you click up arrow that not become 0 but 1. Anyway you had to do the wrong thing and you must fix it..... whatever it isRoman
S
0

I guess the possible solution can be the checking of the subclass of object.

issubclass(hw_obj.__class__, HelloWorld)

Example:

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()
Stomatitis answered 29/12, 2021 at 18:22 Comment(0)
S
0

I came to the same problem. According to this answer https://mcmap.net/q/354865/-how-can-i-mock-patch-a-class-used-in-an-isinstance-test

You can't mock the second argument of isinstance()

I don't know the solution but I know a good workaround:
It's good architecture point to hind all checks (like isintanse) inside separate functions because it's not directly related dependency, in most cases you will change the checks but preserve the behavior.
Put isinstance check into separate function and check it separately.

def logic_checks() -> bool:
    ...
    return isinstance(mock, MyMock) 


def main_func()
    if logic_checks() is not True:
        ...
    ...


def test_main_func()
    logic_checks = Mock(return_value=True)
    main_func()
    logic_checks.assert_called_once_with()
    ...


def test_logic_checks()
    ...  # Here separate checks with another patches, mocks, etc.

Specialize answered 17/12, 2022 at 12:14 Comment(0)
J
0

The way I found to pass isinstance() while magic-mocking an instance (with all attributes and methods) is to modify the dunder class property of the mocked instance:

class Foo(): pass

foo = MagicMock()
foo.__class__ = Foo

isinstance(foo, Foo)  # True
foo.bar = 7  # works
a = foo.baz()  # MagicMock

Hope this helps someone else!

Johppa answered 28/7, 2023 at 17:10 Comment(0)
M
0

I had similar problem. My function checks isinstance for datetime class and uses utcnow mock in other functions in the same module. My solution is similar to @eugene-kovalev (and I based on it):

class DatetimeMock(MagicMock):
    def __instancecheck__(self, instance):
        return isinstance(instance, datetime)


class CreatePlotTestCase(BaseTestCase):

    @patch("core.timezones.datetime", new_callable=DatetimeMock)
    def test_positive(self, datetime_mock):
        """Test on plot creation"""

        now = datetime.now()
        datetime_mock.utcnow.return_value = now
        datetime_mock.now.return_value = now

So my isinstance check handled by __instancecheck__ and checks correctly to the base datetime class - just as I want.

Meritocracy answered 9/4 at 8:34 Comment(0)
C
-1

I think it's safe to use freezegun. All the necessary preparations for correctly mocking the datetime module are made there. Also, the isinstance check does not fail for me.

It works like this:

@freeze_time("2019-05-15")
def test_that_requires_frozen_time(): ...
Checkup answered 19/11, 2019 at 10:34 Comment(0)
C
-7

Don't use isinstance, instead check for the existence of the say_it method. If the method exists, call it:

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

This is a better design anyway: relying on type information is much more brittle.

Caralie answered 21/6, 2012 at 21:9 Comment(10)
Thanks. But the first question would really be why is it throwing that error? And two, adopting this change, if two objs have the same method name, the test will pass, right?Tendency
I don't know why the error is thrown. It doesn't matter, soon you won't be using isinstance :) And yes, now you can pass in any object that has the method, and behaves "properly", and the test will pass.Caralie
Thanks. I will stop using isinstance after reading the major fallback. BUt still... if MyMomKitchenObject has say_it and programmer uses that as the input of the function... the test will still PASS, doesn't it? So how should I verify my unittest actually works? or how should I determine its correctness in my code? Just like integration test, two objs can have 99% same interface, and the system under test never uses that 1% different, and the test still passes, the system will "work without problem".Tendency
Usually, an unittest is not about checking that a method exists, but checking whether it returns what you expect or not.Skilled
Thanks Thomas. But that's not what I was asking. Unittest does the coverage, and using mock it can help us to isolate dependencies from the system under test. But we want to know what happen if someone pass in class X rather than class Y. Is it a good practice to check that in the code? I don't know... I will think for a while and then make a new post on this subject.Tendency
@Tendency Yes, I understood that. What I meant is that Python usually encourages Duck Typing, that is, if your object behaves like you expect it to behave, then it's OK. So, your unittests should focus on the behavior. If someone passes a class that doesn't behave like it should, then their unittests should fail.Skilled
Don't use isinstance() is not a solution for me: I try to mock datetime.datetime and it is used in external libraries. In my case django uses it.Gooseberry
@Tendency I post a answer that fit exactly your question.Roman
Downvote, suggesting to not use isinstance is a terrible answer. Do it right with Mock, much like this: https://mcmap.net/q/354866/-can-i-fake-mock-the-type-of-my-mock-objects-in-python-unittestsDiandiana
Upvote. I came to this question because I wanted to know how to test isinstance. I recognize that Ned's answer does not address the OP's question. However, his answer did help me recognize that my code would be better with try instead of isinstance.Alarice

© 2022 - 2024 — McMap. All rights reserved.