简体   繁体   中英

How do I directly mock a superclass with python mock?

I am using the python mock framework for testing (http://www.voidspace.org.uk/python/mock/) and I want to mock out a superclass and focus on testing the subclasses' added behavior.

(For those interested I have extended pymongo.collection.Collection and I want to only test my added behavior. I do not want to have to run mongodb as another process for testing purposes.)

For this discussion, A is the superclass and B is the subclass. Furthermore, I define direct and indirect superclass calls as shown below:

class A(object):
    def method(self):
        ...

    def another_method(self):
        ...

class B(A):
    def direct_superclass_call(self):
        ...
        A.method(self)

    def indirect_superclass_call(self):
        ...
        super(A, self).another_method()

Approach #1

Define a mock class for A called MockA and use mock.patch to substitute it for the test at runtime. This handles direct superclass calls. Then manipulate B.__bases__ to handle indirect superclass calls. (see below)

The issue that arises is that I have to write MockA and in some cases (as in the case for pymongo.collection.Collection ) this can involve a lot of work to unravel all of the internal calls to mock out.

Approach #2

The desired approach is to somehow use a mock.Mock() class to handle calls on the the mock just in time, as well as defined return_value or side_effect in place in the test. In this manner, I have to do less work by avoiding the definition of MockA.

The issue that I am having is that I cannot figure out how to alter B.__bases__ so that an instance of mock.Mock() can be put in place as a superclass (I must need to somehow do some direct binding here). Thus far I have determined, that super() examines the MRO and then calls the first class that defines the method in question. I cannot figure out how to get a superclass to handle the check to it and succeed if it comes across a mock class. __getattr__ does not seem to be used in this case. I want super to to think that the method is defined at this point and then use the mock.Mock() functionality as usual.

How does super() discover what attributes are defined within the class in the MRO sequence? And is there a way for me to interject here and to somehow get it to utilize a mock.Mock() on the fly?

import mock

class A(object):
    def __init__(self, value):
        self.value = value      

    def get_value_direct(self):
        return self.value

    def get_value_indirect(self):
        return self.value   

class B(A):
    def __init__(self, value):
        A.__init__(self, value)

    def get_value_direct(self):
        return A.get_value_direct(self)

    def get_value_indirect(self):
        return super(B, self).get_value_indirect()


# approach 1 - use a defined MockA
class MockA(object):
    def __init__(self, value):
        pass

    def get_value_direct(self):
        return 0

    def get_value_indirect(self):
        return 0

B.__bases__ = (MockA, )  # - mock superclass 
with mock.patch('__main__.A', MockA):  
    b2 = B(7)
    print '\nApproach 1'
    print 'expected result = 0'
    print 'direct =', b2.get_value_direct()
    print 'indirect =', b2.get_value_indirect()
B.__bases__ = (A, )  # - original superclass 


# approach 2 - use mock module to mock out superclass

# what does XXX need to be below to use mock.Mock()?
#B.__bases__ = (XXX, )
with mock.patch('__main__.A') as mymock:  
    b3 = B(7)
    mymock.get_value_direct.return_value = 0
    mymock.get_value_indirect.return_value = 0
    print '\nApproach 2'
    print 'expected result = 0'
    print 'direct =', b3.get_value_direct()
    print 'indirect =', b3.get_value_indirect() # FAILS HERE as the old superclass is called
#B.__bases__ = (A, )  # - original superclass

is there a way for me to interject here and to somehow get it to utilize a mock.Mock() on the fly?

There may be better approaches, but you can always write your own super() and inject it into the module that contains the class you're mocking. Have it return whatever it should based on what's calling it.

You can either just define super() in the current namespace (in which case the redefinition only applies to the current module after the definition), or you can import __builtin__ and apply the redefinition to __builtin__.super , in which case it will apply globally in the Python session.

You can capture the original super function (if you need to call it from your implementation) using a default argument:

def super(type, obj=None, super=super):  
    # inside the function, super refers to the built-in

I played around with mocking out super() as suggested by kindall. Unfortunately, after a great deal of effort it became quite complicated to handle complex inheritance cases.

After some work I realized that super() accesses the __dict__ of classes directly when resolving attributes through the MRO (it does not do a getattr type of call). The solution is to extend a mock.MagicMock() object and wrap it with a class to accomplish this. The wrapped class can then be placed in the __bases__ variable of a subclass.

The wrapped object reflects all defined attributes of the target class to the __dict__ of the wrapping class so that super() calls resolve to the properly patched in attributes within the internal MagicMock().

The following code is the solution that I have found to work thus far. Note that I actually implement this within a context handler. Also, care has to be taken to patch in the proper namespaces if importing from other modules.

This is a simple example illustrating the approach:

from mock import MagicMock
import inspect 


class _WrappedMagicMock(MagicMock):
    def __init__(self, *args, **kwds):
        object.__setattr__(self, '_mockclass_wrapper', None)
        super(_WrappedMagicMock, self).__init__(*args, **kwds)

    def wrap(self, cls):
        # get defined attribtues of spec class that need to be preset
        base_attrs = dir(type('Dummy', (object,), {}))
        attrs = inspect.getmembers(self._spec_class)
        new_attrs = [a[0] for a in attrs if a[0] not in base_attrs]

        # pre set mocks for attributes in the target mock class
        for name in new_attrs:
            setattr(cls, name, getattr(self, name))

        # eat up any attempts to initialize the target mock class
        setattr(cls, '__init__', lambda *args, **kwds: None)

        object.__setattr__(self, '_mockclass_wrapper', cls)

    def unwrap(self):
        object.__setattr__(self, '_mockclass_wrapper', None)

    def __setattr__(self, name, value):
        super(_WrappedMagicMock, self).__setattr__(name, value)

        # be sure to reflect to changes wrapper class if activated
        if self._mockclass_wrapper is not None:
            setattr(self._mockclass_wrapper, name, value)

    def _get_child_mock(self, **kwds):
        # when created children mocks need only be MagicMocks
        return MagicMock(**kwds)


class A(object):
    x = 1

    def __init__(self, value):
        self.value = value

    def get_value_direct(self):
        return self.value

    def get_value_indirect(self):
        return self.value


class B(A):
    def __init__(self, value):
        super(B, self).__init__(value)

    def f(self):
        return 2

    def get_value_direct(self):
        return A.get_value_direct(self)

    def get_value_indirect(self):
        return super(B, self).get_value_indirect()

# nominal behavior
b = B(3)
assert b.get_value_direct() == 3
assert b.get_value_indirect() == 3
assert b.f() == 2
assert b.x == 1

# using mock class
MockClass = type('MockClassWrapper', (), {})
mock = _WrappedMagicMock(A)
mock.wrap(MockClass)

# patch the mock in
B.__bases__ = (MockClass, )
A = MockClass

# set values within the mock
mock.x = 0
mock.get_value_direct.return_value = 0
mock.get_value_indirect.return_value = 0

# mocked behavior
b = B(7)
assert b.get_value_direct() == 0
assert b.get_value_indirect() == 0
assert b.f() == 2
assert b.x == 0

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM