22

I want to mock a method of a class and use wraps, so that it is actually called, but I can inspect the arguments passed to it. I have seen at several places (here for example) that the usual way to do that is as follows (adapted to show my point):

from unittest import TestCase
from unittest.mock import patch


class Potato(object):
    def foo(self, n):
        return self.bar(n)

    def bar(self, n):
        return n + 2


class PotatoTest(TestCase):
    spud = Potato()

    @patch.object(Potato, 'foo', wraps=spud.foo)
    def test_something(self, mock):
        forty_two = self.spud.foo(n=40)
        mock.assert_called_once_with(n=40)
        self.assertEqual(forty_two, 42)

However, this instantiates the class Potato, in order to bind the mock to the instance method spud.foo.

What I need is to mock the method foo in all instances of Potato, and wrap them around the original methods. I.e, I need the following:

from unittest import TestCase
from unittest.mock import patch


class Potato(object):
    def foo(self, n):
        return self.bar(n)

    def bar(self, n):
        return n + 2


class PotatoTest(TestCase):
    @patch.object(Potato, 'foo', wraps=Potato.foo)
    def test_something(self, mock):
        self.spud = Potato()
        forty_two = self.spud.foo(n=40)
        mock.assert_called_once_with(n=40)
        self.assertEqual(forty_two, 42)

This of course doesn't work. I get the error:

TypeError: foo() missing 1 required positional argument: 'self'

It works however if wraps is not used, so the problem is not in the mock itself, but in the way it calls the wrapped function. For example, this works (but of course I had to "fake" the returned value, because now Potato.foo is never actually run):

from unittest import TestCase
from unittest.mock import patch


class Potato(object):
    def foo(self, n):
        return self.bar(n)

    def bar(self, n):
        return n + 2


class PotatoTest(TestCase):
    @patch.object(Potato, 'foo', return_value=42)#, wraps=Potato.foo)
    def test_something(self, mock):
        self.spud = Potato()
        forty_two = self.spud.foo(n=40)
        mock.assert_called_once_with(n=40)
        self.assertEqual(forty_two, 42)

This works, but it does not run the original function, which I need to run because the return value is used elsewhere (and I cannot fake it from the test).

Can it be done?

Note The actual reason behind my needs is that I'm testing a rest api with webtest. From the tests I perform some wsgi requests to some paths, and my framework instantiates some classes and uses their methods to fulfill the request. I want to capture the parameters sent to those methods to do some asserts about them in my tests.

JLDiaz
  • 1,248
  • 9
  • 21
  • I saw this code recently. https://github.com/keleshev/value/blob/master/value.py involving `from inspect import getargspec`and `__new__` - not sure if that can be adapted to your needs. I'm pretty new to Python and haven't used patch... I believe this would allow you to inspect arguments – JGFMK May 19 '18 at 00:29
  • @JGFMK Thank you, but that seems unrelated. Apparently the code you linked is for inspecting the list of parameters of a function, even whitout calling it, but what I need is to capture the values of the actual arguments when the function is called. – JLDiaz May 19 '18 at 09:20
  • How about this one? https://bitbucket.org/jsbueno/metapython/src/f48d6bd388fd/aspect.py?fileviewer=file-view-default - Got this by Googling AOP (Aspect Orientated Programming) in Python – JGFMK May 19 '18 at 19:37
  • What if you wrap `foo` in a function and pass it function to `wraps`? – Mauro Baraldi Jun 12 '18 at 12:59
  • Does this answer your question? [python mock - patching a method without obstructing implementation](https://stackoverflow.com/questions/25608107/python-mock-patching-a-method-without-obstructing-implementation) – Benjamin Drung May 31 '22 at 11:31

3 Answers3

6

In short, you can't do this using Mock instances alone.

patch.object creates Mock's for the specified instance (Potato), i.e. it replaces Potato.foo with a single Mock the moment it is called. Therefore, there is no way to pass instances to the Mock as the mock is created before any instances are. To my knowledge getting instance information to the Mock at runtime is also very difficult.

To illustrate:

from unittest.mock import MagicMock

class MyMock(MagicMock):
    def __init__(self, *a, **kw):
        super(MyMock, self).__init__(*a, **kw)
        print('Created Mock instance a={}, kw={}'.format(a,kw))

with patch.object(Potato, 'foo', new_callable=MyMock, wrap=Potato.foo):
    print('no instances created')
    spud = Potato()
    print('instance created')

The output is:

Created Mock instance a=(), kw={'name': 'foo', 'wrap': <function Potato.foo at 0x7f5d9bfddea0>}
no instances created
instance created

I would suggest monkey-patching your class in order to add the Mock to the correct location.

from unittest.mock import MagicMock

class PotatoTest(TestCase):
    def test_something(self):

        old_foo = Potato.foo
        try:
            mock = MagicMock(wraps=Potato.foo, return_value=42)
            Potato.foo = lambda *a,**kw: mock(*a, **kw)

            self.spud = Potato()
            forty_two = self.spud.foo(n=40)
            mock.assert_called_once_with(self.spud, n=40) # Now needs self instance
            self.assertEqual(forty_two, 42)
        finally:
            Potato.foo = old_foo

Note that you using called_with is problematic as you are calling your functions with an instance.

Dave Evans
  • 93
  • 1
  • 8
1

Your question looks identical to python mock - patching a method without obstructing implementation to me. https://stackoverflow.com/a/72446739/9230828 implements what you want (except that it uses a with statement instead of a decorator). wrap_object.py:

# Copyright (C) 2022, Benjamin Drung <bdrung@posteo.de>
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

import contextlib
import typing
import unittest.mock

@contextlib.contextmanager
def wrap_object(
    target: object, attribute: str
) -> typing.Generator[unittest.mock.MagicMock, None, None]:
    """Wrap the named member on an object with a mock object.

    wrap_object() can be used as a context manager. Inside the
    body of the with statement, the attribute of the target is
    wrapped with a :class:`unittest.mock.MagicMock` object. When
    the with statement exits the patch is undone.

    The instance argument 'self' of the wrapped attribute is
    intentionally not logged in the MagicMock call. Therefore
    wrap_object() can be used to check all calls to the object,
    but not differentiate between different instances.
    """
    mock = unittest.mock.MagicMock()
    real_attribute = getattr(target, attribute)

    def mocked_attribute(self, *args, **kwargs):
        mock.__call__(*args, **kwargs)
        return real_attribute(self, *args, **kwargs)

    with unittest.mock.patch.object(target, attribute, mocked_attribute):
        yield mock

Then you can write following unit test:

from unittest import TestCase

from wrap_object import wrap_object


class Potato:
    def foo(self, n):
        return self.bar(n)

    def bar(self, n):
        return n + 2


class PotatoTest(TestCase):
    def test_something(self):
        with wrap_object(Potato, 'foo') as mock:
            self.spud = Potato()
            forty_two = self.spud.foo(n=40)
        mock.assert_called_once_with(n=40)
        self.assertEqual(forty_two, 42)
0

Do you control creation of Potato instances, or at least have access to these instances after creating them? You should, else you'd not be able to check particular arg lists. If so, you can wrap methods of individual instances using

spud = dig_out_a_potato()
with mock.patch.object(spud, "foo", wraps=spud.foo) as mock_spud:
  # do your thing.
  mock_spud.assert_called...
9000
  • 39,899
  • 9
  • 66
  • 104