Substitute a mock object for the metaclass of a class

Matt Wheeler m at funkyhat.org
Mon Mar 13 18:36:06 EDT 2017


On Mon, 13 Mar 2017 at 00:52 Ben Finney <ben+python at benfinney.id.au> wrote:

> How can I override the metaclass of a Python class, with a
> `unittest.mock.MagicMock` instance instead?
>

At first I misunderstood what you were looking for, and was about to reply
to the effect of "you're too late, the metaclass has already been called so
this doesn't make sense", but I see that you're actually asking for
something that's a bit interesting...


> I have a function whose job involves working with the metaclass of an
> argument::
>
>     # lorem.py
>
>     class Foo(object):
>         pass
>
>     def quux(existing_class):
>>         metaclass = type(existing_class)
>         new_class = metaclass(…)
>
> The unit tests for this function will need to assert that the calls to
> the metaclass go as expected, *without* actually calling a real class
> object.
>
> To write a unit test for this function, I want to temporarily override
> the metaclass of some classes in the system with a mock object instead.
> This will allow, for example, making assertions about how the metaclass
> was called, and ensuring no unwanted side effects.
>

The simple, lazy option would be to patch `type` to return your mocked
metaclass. i.e.

```
from unittest.mock import patch

import lorem


@patch('lorem.type')
def test_things(mocktype):
    lorem.quux(metameta.Foo())

    lorem.return_value.assert_called_with()
```

Don't try and patch `type` from `builtins`. I did, and things go bad
because, unsurprisingly, `mock` calls `type` quite a lot internally :).


>     # test_lorem.py
>
>     import unittest
>     import unittest.mock
>
>     import lorem
>
>     class stub_metaclass(type):
>         def __new__(metaclass, name, bases, namespace):
>             return super().__new__(metaclass, name, bases, namespace)
>
>     class quux_TestCase(unittest.TestCase):
>
>         @unittest.mock.patch.object(
>                     lorem.Foo, '__class__', side_effect=stub_metaclass)
>         def test_calls_expected_metaclass_with_class_name(
>                 self,
>                 mock_foo_metaclass,
>                 ):
>             lorem.quux(lorem.Foo)
>             mock_foo_metaclass.assert_called_with(
>                     'Foo', unittest.mock.ANY, unittest.mock.ANY)
>

When I try to add the stub_metaclass side_effect in to my code I get
`TypeError: __new__() missing 2 required positional arguments: 'bases' and
'namespace'` ... which seems quite reasonable, and I expect you're in a
better position to figure out how to handle that (the side effect may
actually need to be a wrapper around stub_metaclass which injects something
suitable).

<rest of original snipped>
-- 

--
Matt Wheeler
http://funkyh.at


More information about the Python-list mailing list