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:
spec parameter to the real class. __class__ attribute. 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 anisinstance()check without forcing you to use a spec:>>> mock = Mock() >>> mock.__class__ = dict >>> isinstance(mock, dict) True[...]
If you use
specorspec_setandpatch()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
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:
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.MyClass is left in-tact and is booby trapped.__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.isinstance(obj, Original) to check the instance, so you never got the type error there. The type error is instead triggered in is_myclass().If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With