Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Patching superclass methods with mocks

There are a number of similar(ish) questions here about how, in Python, you are supposed to patch the superclasses of your class, for testing. I've gleaned some ideas from them, but I'm still not where I need to be.

Imagine I have two base classes:

class Foo(object):
    def something(self, a):
        return a + 1

class Bar(object):
    def mixin(self):
        print("Hello!")

Now I define the class that I want to test as such:

class Quux(Foo, Bar):
    def something(self, a):
        self.mixin()
        return super().something(a) + 2

Say I want to test that mixin has been called and I want to replace the return value of the mocked Foo.something, but importantly (and necessarily) I don't want to change any of the control flow or logic in Quux.something. Presuming patching superclasses "just worked", I tried unittest.mock.patch:

with patch("__main__.Foo", spec=True) as mock_foo:
    with patch("__main__.Bar", spec=True) as mock_bar:
        mock_foo.something.return_value = 123
        q = Quux()
        assert q.something(0) == 125
        mock_bar.mixin.assert_called_once()

This doesn't work: The superclasses' definitions of something and mixin aren't being mocked when Quux is instantiated, which is not unsurprising as the class' inheritance is defined before the patch.

I can get around the mixin problem, at least, by explicitly setting it:

# This works to mock the mixin method
q = Quux()
setattr(q, "mixin", mock_bar.mixin)

However, a similar approach doesn't work for the overridden method, something.

As I mentioned, other answers to this question suggest overriding Quux's __bases__ value with the mocks. However, this doesn't work at all as __bases__ must be a tuple of classes and the mocks' classes appear to just be the originals:

# This doesn't do what I want
Quux.__bases__ = (mock_foo.__class__, mock_bar.__class__)
q = Quux()

Other answers suggested overriding super. This does work, but I feel that it's a bit dangerous as any calls to super you don't want to patch will probably break things horribly.

So is there a better way of doing what I want than this:

with patch("builtins.super") as mock_super:
    mock_foo = MagicMock(spec=Foo)
    mock_foo.something.return_value = 123
    mock_super.return_value = mock_foo

    mock_bar = MagicMock(spec=Bar)

    q = Quux()
    setattr(q, "mixin", mock_bar.mixin)

    assert q.something(0) == 125
    mock_bar.mixin.assert_called_once()
like image 916
Xophmeister Avatar asked Nov 18 '22 11:11

Xophmeister


1 Answers

The matter is actually simple - the subclass will contain a reference to the original classes inside its own structure (the public visible attributes __bases__ and __mro__). That reference is not changed when you mock those base classes - the mocking would only affect one using those objects explicitly, while the patching is "turned on". In other words, they would only be used if your Quux class would itself be defined inside the with blocks. And that would not work either, as the "mock" object replacing the classes can not be a proper superclass.

However, the workaround, and the right way to do it are quite simple - you just have to mock the methods you want replaced, not the classes.

The question is a bit old now, and I hope you had moved on, but the right thing to do there is:

with patch("__main__.Foo.something", spec=True) as mock_foo:
    with patch("__main__.Bar.mixin", spec=True) as mock_bar:
        mock_foo.return_value = 123
        q = Quux()
        assert q.something(0) == 125
        mock_bar.assert_called_once()

like image 200
jsbueno Avatar answered Jan 31 '23 08:01

jsbueno