How to spy on Mock-wrapped method calls in Python

You have a class whose API you want to verify:

    class Class_A(object):
        def method_X(self):
            self.method_Y()

        def method_Y(self):
            pass

You write the following test fully expecting it to pass. Alas, the method_Y assertion fails.

    def test_class_a(self):
        obj = Class_A()
        mock = MagicMock(spec_set=obj, wraps=obj)

        mock.method_X()
        mock.method_Y.assert_called_once_with()

This is because the Python Unittest Mock, while wrapping mock.method_X(), Mock uses obj in the __self__ of the method_Y binding. As a result the self value passed to method_X is that of the obj and not mock and the call to method_Y is therefore invisible to the Mock.


Enter Spy available from Karellen Testing Library (PyPI), which solves the above problem by substituting wrapped methods in the Mock by rebinding them through Spy object, passing Spy object as self to all method calls. Since the mock wraps the Class_A object and Spy wraps the mock, Spy ensures that all internal object calls go back through the mock, allowing for call accounting and argument capture. All getattr and setattr calls end up going through the Spy as well, allowing it to either retrieve the attribute of the wrapped object or delegate to the mock instead.

The magic of solution is in this line rebinding the wrapped method to use the Spy for __self__ with the rest performing field housekeeping and mock delegation as appropriate:

attr._mock_wraps = types.MethodType(mock_wraps.__func__, self)

This spying method should work for most common objects. The properly working test code will now look like and will pass.

from karellen.testing.mock import MagicSpy

[...]

    def test_class_a_spy(self):
        mock = MagicSpy(Class_A())

        mock.method_X()
        mock.method_Y.assert_called_once_with()

MagicSpy(obj) is equivalent to:

mock = MagicMock(spec_set=obj, wraps=obj)
return Spy(mock)

Leave a Reply