Quantcast
Channel: Jeremy Satterfield
Viewing all articles
Browse latest Browse all 39

Keeping MRO In Mind When Mocking Inherited Methods

$
0
0

So I just spent a couple of hours banging my head on a ridiculous PyMox mocking issue and though I'd share. Here is an example of the existing code.

# views.py
class MyBaseView(BaseView):
def get_stuff(self):
# this is doing things
class SpecificView(MyBaseView):
example = True
def send_stuff(self):
stuff = self.get_stuff()
# do other things
# tests.py
class SpecificViewTest(TestCase):
def setUp(self):
self.view = SpecificView()
self.mox = mox.Mox()
def tearDown(self):
self.mox.UnsetStubs()
def test_send_stuff(self):
self.mox.StubOutWithMock(MyBaseView, 'get_stuff')
MyBaseView.get_stuff().AndReturn(['list', 'of', 'things'])
self.mox.ReplayAll()
self.view.send_stuff()
self.mox.VerifyAll()
# do assertions

This was all fine and working for months, until I added the following.

# views.py
class ExtraSpecificView(SpecificView):
def other_stuff(self):
self.get_stuff()
# do more things again
# test.py
class ExtraSpecificViewTest(TestCase):
def setUp(self):
self.view = ExtraSpecificView()
self.mox = mox.Mox()
def tearDown(self):
self.mox.UnsetStubs()
def test_other_stuff(self):
self.mox.StubOutWithMox(SpecifcView, 'get_stuff')
SpecificView.get_stuff().AndReturn(['more', 'things'])
self.mox.ReplayAll()
self.view.other_stuff()
self.mox.VerifyAll()

So what started happening at this point is SpecificViewTest.test_send_stuff began failing with this.

ExpectedMethodCallsError: Verify: Expected methods never called:
0. get_stuff.__call__() -> ['list', 'of', 'things']

It would only fail when ExtraSpecificViewTest ran before SpecificViewTest which, with nose, it does by default. So I knew it had to have something to do with the mocking in ExtraSpecificViewTest.test_other_stuff. After more playing and banging my head I came up with this working theory which seems correct the more I think on it.

When you mock out a method on a class, the mocking library stores the original method and replaces it with the mock function. When you tell the library to stop mocking the method, mox.UnsetStubs in this case, it grabs that original method it stored before and assigns it back to the appropriate attribute on the class, restoring it's original functionality.

However, in my class I was mocking a method that SpecificView inherited from MyBaseView. When the library grabs the original method for storing Python sees that SpecificView doesn't have a get_stuff method of it's own, so it moves up the MRO and grabs MyBaseView.get_stuff and the library stores that and assigns a mock function to the get_stuff attribute on the class of SpecificView. Then, when the library goes to un-stub the method, it grabs the stored get_stuff Python gave it from MyBaseView and assigns it also to the get_stuff attribute of the class of SpecificView. Finally, when SpecificViewTest.test_set_stuff runs, send_stuff calls SpecificView's get_stuff, instead of moving up the MRO as to used to, Python now sees that the SpecificView class has it's own get_stuff method (which doesn't have a super call), so it doesn't move up the MRO, therefore MyBaseView is never called as expected and raising a failure when verified.

So this became the fix.

#tests.py
class ExtraSpecificViewTest(TestCase):
...
def test_other_stuff(self):
self.mox.StubOutWithMox(MyBaseView, 'get_stuff')
MyBaseView.get_stuff().AndReturn(['more', 'things'])

It's a convoluted bug that may or may not be specific to PyMox or other mocking libraries, but I'm not sure how they should be properly handling something like this. So I guess the moral of the story is to mock methods from the class which the were originally defined, not just the class you inherited from.


Viewing all articles
Browse latest Browse all 39

Trending Articles