Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Patching a parent class

I'm having trouble getting a parent class mocked with mock.patch.

Here's a test case:

In parent.py:

import mock

class Parent():
    def __init__(self):
        print("Original recipe")

In child.py:

from parent import Parent

class Child(Parent):
    def foo(self):
        print('Parent is {}'.format(Parent))

In test.py:

import mock

from child import Child

c = Child()  # expect 'Original recipe'
c.foo()

with mock.patch('child.Parent'):
    c = Child() # expect silence
    c.foo()

When I run test.py I expect to get:

Original recipe
Parent is <class 'parent.Parent'>
Parent is <MagicMock name='Parent' id='4325705712'>    

but instead I get:

Original recipe
Parent is <class 'parent.Parent'>
Original recipe
Parent is <MagicMock name='Parent' id='4325705712'>

So the patch is happening (from the "Parent is" statement) but not for the class inheritance. How can I fix that?

like image 759
Turn Avatar asked Aug 13 '16 01:08

Turn


People also ask

Can you inherit child class parent class?

Inheritance concept is to inherit properties from one class to another but not vice versa. But since parent class reference variable points to sub class objects. So it is possible to access child class properties by parent class object if only the down casting is allowed or possible....

How do you define a parent class?

Parent class is the class being inherited from, also called base class. Child class is the class that inherits from another class, also called derived class.

What is side_ effect in mock Python?

side_effect: A function to be called whenever the Mock is called. See the side_effect attribute. Useful for raising exceptions or dynamically changing return values. The function is called with the same arguments as the mock, and unless it returns DEFAULT , the return value of this function is used as the return value.

How to mock a method in Python?

How do we mock in Python? Mocking in Python is done by using patch to hijack an API function or object creation call. When patch intercepts a call, it returns a MagicMock object by default. By setting properties on the MagicMock object, you can mock the API call to return any value you want or raise an Exception .


1 Answers

You are not patching the Parent class, you are patching the child module, changing its Parent attribute to a mock. This does not change Child at all, because it still uses the old Parent class as base class.

In Python 3, you can instead patch Child.__bases__ to change the base class at runtime. This come with its weirdnesses, of course.

Additional details

Python has no "variables", only names bound to specific objects in memory. Changing those names bindings (e.g. patching the scope they are contained it with mock.patch or setattr) has absolutely no effect on previous uses of these bindings.

This means that although you do patch the Parent attribute of the child module, replacing it by a Mock, as the module has already loaded, the class is already defined with the old target of the Parent attribute, which is, the original Parent class.

Other ways to test Child instances

As if Parent as external

with mock.patch('child.Child.method_that_calls_method_on_parent'):
    ...

If you want to isolate and mock method on Parent when testing instances of Child, you could make the calls to Parent sit in dedicated methods (and then patch these methods), as you'd do test external classes.

Patching methods on Parent

If you know in advance which methods of Parent you'll need to patch, you can just imply patch the methods on Parent.

with mock.patch('parent.Parent.method'):
    ...

This will mutate the value of the Parent attribute (which is the same for the child and parent modules, as child imports Parent from parent), instead of modifying which objects the Parent attribute points to in a particular module as you were doing before.

Making Parent behave like a Mock

with mock.patch('parent.Parent.__getattribute__'):
    ...

This is the most close to the intent of your original code. It relies on changing the way Python gets attributes from the Parent class, effectively patching all of its possible attributes.

The disadvantage with this is that you'd get a mock even for non-existing attributes, but that was the case with your original approach as well. This can be overcome by replacing __getattribute__ with a wrapper that returns a mock only for found attributes :

def _getmock(self, name):
    value = object.__getattribute__(self, name)
    return Mock(value)
_original = getattr(parent.Parent, '__getattribute__')
setattr(parent.Parent, '__getattribute__', _getmock)
try:
    ...
finally:
    setattr(parent.Parent, '__getattribute__', _original)

(Your test suite probably provides a way to temporarily patch _getmock as parent.Parent.__getattribute__, as mock.patch does, which would make this simpler.)

This can be further customized to specify the type and parameters of the mock created depending on the attribute name (name in _getmock) or value (value in _getmock), or making it so that the same mock is returned for when the same attribute name is accessed multiple times.

like image 53
pistache Avatar answered Oct 09 '22 14:10

pistache