How can I mock patch a class used in an isinstance test?
mocking, python, python-unittest, unit-testing
Solution
You can't mock the second argument of `isinstance()`, no. The documentation you found concerns making a mock as the first argument pass the test. If you want to produce something that is acceptable as the second argument to `isinstance()`, you actually have to have a type, not an instance (and mocks are always instances).
You could use a subclass instead of `MyClass` instead, that'll definitely pass, and giving it a `__new__` method lets you alter what is returned when you try to call it to create an instance:
class MockedSubClass(MyClass):
def __new__(cls, *args, **kwargs):
return mock.Mock(spec=cls) # produce a mocked instance when called
and patch that in:
mock.patch('temp2.MyClass', new=MockedSubClass)
and use an instance of that class as the mock:
instance = mock.Mock(spec=MockedSubClass)
Or, and this is much simpler, just use `Mock` as the class, and have `obj` be an `Mock` instance:
with mock.patch('temp2.MyClass', new=mock.Mock) as mocked_class:
is_myclass(mocked_class())
Either way, your test then passes:
>>> with mock.patch('temp2.MyClass', new=MockedSubClass) as mocked_class:
... instance = mock.Mock(spec=MockedSubClass)
... assert isinstance(instance, mocked_class)
... is_myclass(instance)
...
>>> # no exceptions raised!
...
>>> with mock.patch('temp2.MyClass', new=mock.Mock) as mocked_class:
... is_myclass(mocked_class())
...
>>> # no exceptions raised!
...
For your specific tests, here is why they fail:
- You never mocked `MyClass`, it still references the original class. The first line of `is_myclass()` succeeds, but the second line uses the original `MyClass` and it is booby trapped.
- `MyClass` is replaced with a `mock.Mock` instance, not an actual type, so `isinstance()` raises a `TypeError: isinstance() arg 2 must be a type or tuple of types` exception.
- Fails exactly the same way 1 failed, `MyClass` is left in-tact and is booby trapped.
- Fails the same way as 2 does. `__class__` is an attribute only useful on instances. A class object doesn't use the `__class__` attribute, you still have an instance instead of a class and `isinstance()` raises a type error.
- is essentially exactly the same as 4, only you started the patcher manually instead of having the context manager take care of it, and you used `isinstance(obj, Original)` to check the instance, so you never got the type error there. The type error is instead triggered in `is_myclass()`.
Problem
I want to test the function `is_myclass`. Please help me understand how to write a successful test. ``` def is_myclass(obj): """This absurd stub is a simplified version of the production code.""" isinstance(obj, MyClass) MyClass() ``` Docs The Python Docs for unittest.mock illustrate three ways of addressing the `isinstance` problem: - Set the `spec` parameter to the real class. - Assign the real class to the `__class__` attribute. - Use `spec` in the patch of the real class. `__class__` 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 ``` `__class__` is assignable to, this allows a mock to pass an `isinstance()` check without forcing you to use a spec: ``` >>> mock = Mock() >>> mock.__class__ = dict >>> isinstance(mock, dict) True ``` [...] If you use `spec` or `spec_set` and `patch()` is replacing a class, then the return value of the created mock will have the same spec. ``` >>> Original = Class >>> patcher = patch('__main__.Class', spec=True) >>> MockClass = patcher.start() >>> instance = MockClass() >>> assert isinstance(instance, Original) >>> patcher.stop() ``` Tests I have written five tests each of which attempts firstly to reproduce each of the three solutions and secondly to carry out a realistic test of the target code. The typical pattern is `assert isinstance` followed by a call to `is_myclass`. All tests fail. Test 1 This is a close copy of the example provided in the docs for the use of `spec`. It differs from the docs by using `spec=<class>` instead of `spec=<instance>`. It passes a local assert test but the call to `is_myclass` fails because `MyClass` is not mocked. This is equivalent to Michele d’Amico’s answer to the similar question in isinstance and Mocking . Test 2 This is the patched equivalent of test 1. The `spec` argument fails to set the `__class__` of the mocked MyClass and the test fails the local `assert isinstance`. Test 3 This is a close copy of the example provided in the docs for the use of `__class__`. It passes a local assert test but the call to `is_myclass` fails because `MyClass` is not mocked. Test 4 This is the patched equivalent of test 3. The assignment to `__class__` does set the `__class__` of the mocked `MyClass` but this does not change its type and so the test fails the local `assert isinstance`. Test 5 This is a close copy of the use of `spec` in a call to patch. It passes the local assert test but only by virtue of accessing a local copy of MyClass. Since this local variable is not used within `is_myclass` the call fails. Code This code was written as a stand alone test module intended to be run in the PyCharm IDE. You may need to modify it to run in other test environments. module temp2.py ``` import unittest import unittest.mock as mock class WrongCodeTested(Exception): pass class MyClass: def __init__(self): """This is a simplified version of a production class which must be mocked for unittesting.""" raise WrongCodeTested('Testing code in MyClass.__init__') def is_myclass(obj): """This absurd stub is a simplified version of the production code.""" isinstance(obj, MyClass) MyClass() class ExamplesFromDocs(unittest.TestCase): def test_1_spec(self): obj = mock.Mock(spec=MyClass) print(type(MyClass)) # <class 'type'> assert isinstance(obj, MyClass) # Local assert test passes is_myclass(obj) # Fail: MyClass instantiated def test_2_spec_patch(self): with mock.patch('temp2.MyClass', spec=True) as mock_myclass: obj = mock_myclass() print(type(mock_myclass)) # <class 'unittest.mock.MagicMock'> print(type(MyClass)) # <class 'unittest.mock.MagicMock'> assert isinstance(obj, MyClass) # Local assert test fails def test_3__class__(self): obj = mock.Mock() obj.__class__ = MyClass print(type(MyClass)) # <class 'type'> isinstance(obj, MyClass) # Local assert test passes is_myclass(obj) # Fail: MyClass instantiated def test_4__class__patch(self): Original = MyClass with mock.patch('temp2.MyClass') as mock_myclass: mock_myclass.__class__ = Original obj = mock_myclass() obj.__class__ = Original print(MyClass.__class__) # <class 'temp2.MyClass'> print(type(MyClass)) # <class 'unittest.mock.MagicMock'> assert isinstance(obj, MyClass) # Local assert test fails def test_5_patch_with_spec(self): Original = MyClass p = mock.patch('temp2.MyClass', spec=True) MockMyClass = p.start() obj = MockMyClass() print(type(Original)) # <class 'type'> print(type(MyClass)) # <class 'unittest.mock.MagicMock'> print(type(MockMyClass)) # <class 'unittest.mock.MagicMock'> assert isinstance(obj, Original) # Local assert test passes is_myclass(obj) # Fail: Bad type for MyClass ```