Search code examples
pythonpython-2.7python-unittestpython-mock

Mock class used in isinstance checks which has dynamic attributes


Some classes define their attributes (aka fields) at the class level (outside __init__ or any other function). Some classes define them inside their __init__ function or even from other functions. Some classes use both approaches.

class MyClass(object):
  foo = 'foo'
  def __init__(self, *args, **kwargs):
    self.bar = 'bar'

The problem is, when you use dir, it only includes 'bar' if you pass in an instance of the class.

>>> dir(MyClass)
['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'foo']
>>> myInstance = MyClass()
>>> dir(myInstance)
['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'bar', 'foo']

(scroll to the far right to see the diff)

I have a situation where I need to avoid instantiating MyClass but I want to use it as the spec setting (to pass an isinstance check) in a mock.patch call in a unit test.

@mock.patch('mypackage.MyClass', spec=MyClass)
def test_thing_that_depends_on_MyClass(self, executeQueryMock):
  # uses 'thing' here, which uses MyClass.bar ...

Doing this causes:

AttributeError: Mock object has no attribute 'bar'

This makes sense because the mock docs says:

spec: This can be either a list of strings or an existing object (a class or instance) that acts as the specification for the mock object. If you pass in an object then a list of strings is formed by calling dir on the object (excluding unsupported magic attributes and methods). Accessing any attribute not in this list will raise an AttributeError.

Even if I do instantiate MyClass, I get a different error.

@mock.patch('mypackage.MyClass', spec=MyClass())
def test_thing_that_depends_on_MyClass(self, executeQueryMock):
  # uses 'thing' here, which uses MyClass.bar ...

Causes:

TypeError: 'NonCallableMagicMock' object is not callable

I don't really care about being strict with which functions/attributes I allow accessing of; I actually want the normal MagicMock behaviour which lets you call anything without AttributeError. It seems like using spec makes this strict even though I am just using spec to pass isinstance check.

Question:

How do I properly mock this class which is used in isinstance checks and has attributes which are not defined at the class level?


Solution

  • Given that you just want to pass isinstance checks, I think the simplest solution is to write a quick wrapper to mock.patch.object that sets the __class__ attribute of the returned mock.

    def my_patch(obj, attr_name):
        fake_class = mock.MagicMock()
        fake_instance = fake_class.return_value  # calling a class returns an instance.
        fake_instance.__class__ = getattr(obj, attr_name)
        return mock.patch.object(obj, attr_name, fake_class)
    

    It's used like mock.patch.object:

    @my_patch(some_module, 'MyClass')
    def test_something(self, fake_my_class):
        ...
    

    but the fake object should pass isinstance checks the same way that a spec'd mock would.