Search code examples
pythonpython-2.7python-unittestpython-mock

Inheriting a patched class


I have a base class extending unittest.TestCase, and I want to patch that base class, such that classes extending this base class will have the patches applied as well.

Code Example:

@patch("some.core.function", mocked_method)
class BaseTest(unittest.TestCase):
      #methods
      pass

class TestFunctions(BaseTest):
      #methods
      pass

Patching the TestFunctions class directly works, but patching the BaseTest class does not change the functionality of some.core.function in TestFunctions.


Solution

  • You probably want a metaclass here: a metaclass simply defines how a class is created. By default, all classes are created using Python's built-in class type:

    >>> class Foo:
    ...     pass
    ...
    >>> type(Foo)
    <class 'type'>
    >>> isinstance(Foo, type)
    True
    

    So classes are actually instances of type. Now, we can subclass type to create a custom metaclass (a class that creates classes):

    class PatchMeta(type):
        """A metaclass to patch all inherited classes."""
    

    We need to control the creation of our classes, so we wanna override the type.__new__ here, and use the patch decorator on all new instances:

    class PatchMeta(type):
        """A metaclass to patch all inherited classes."""
    
        def __new__(meta, name, bases, attrs):
            cls = type.__new__(meta, name, bases, attrs)
            cls = patch("some.core.function", mocked_method)(cls)
            return cls
    

    And now you simply set the metaclass using __metaclass__ = PatchMeta:

    class BaseTest(unittest.TestCase):
        __metaclass__ = PatchMeta
        # methods
    

    The issue is this line:

    cls = patch("some.core.function", mocked_method)(cls)
    

    So currently we always decorate with arguments "some.core.function" and mocked_method. Instead you could make it so that it uses the class's attributes, like so:

    cls = patch(*cls.patch_args)(cls)
    

    And then add patch_args to your classes:

    class BaseTest(unittest.TestCase):
        __metaclass__ = PatchMeta
        patch_args = ("some.core.function", mocked_method)
    

    Edit: As @mgilson mentioned in the comments, patch() modifies the class's methods in place, instead of returning a new class. Because of this, we can replace the __new__ with this __init__:

    class PatchMeta(type):
        """A metaclass to patch all inherited classes."""
    
        def __init__(cls, *args, **kwargs):
            super(PatchMeta, self).__init__(*args, **kwargs)
            patch(*cls.patch_args)(cls)
    

    Which is quite unarguably cleaner.