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
spec
orspec_set
andpatch()
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