Python 模拟修补方法而不妨碍实现

是否有一种干净的方法来修补一个对象,以便在测试用例中获得 assert_call*助手,而不需要实际删除操作?

例如,如何修改 @patch行使下列测试通过:

from unittest import TestCase
from 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')
def test_something(self, mock):
spud = Potato()
forty_two = spud.foo(n=40)
mock.assert_called_once_with(n=40)
self.assertEqual(forty_two, 42)

我也许可以使用 side_effect把它们组合在一起,但是我希望能有一种更好的方法,可以在所有函数、类方法、 staticmethod、 unbound 方法等等上以同样的方式工作。

42098 次浏览

Similar solution with yours, but using wraps:

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

According to the documentation:

wraps: Item for the mock object to wrap. If wraps is not None then calling the Mock will pass the call through to the wrapped object (returning the real result). Attribute access on the mock will return a Mock object that wraps the corresponding attribute of the wrapped object (so attempting to access an attribute that doesn’t exist will raise an AttributeError).


class Potato(object):


def spam(self, n):
return self.foo(n=n)


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


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




class PotatoTest(TestCase):


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

This answer address the additional requirement mentioned in the bounty from user Quuxplusone:

The important thing for my use-case is that it work with @patch.mock, i.e. that it not require me to insert any code in between my constructing of the instance of Potato (spud in this example) and my calling of spud.foo. I need spud to be created with a mocked-out foo method from the get-go, because I do not control the place where spud is created.

The use case described above could be achieved without too much trouble by using a decorator:

import unittest
import unittest.mock  # Python 3


def spy_decorator(method_to_decorate):
mock = unittest.mock.MagicMock()
def wrapper(self, *args, **kwargs):
mock(*args, **kwargs)
return method_to_decorate(self, *args, **kwargs)
wrapper.mock = mock
return wrapper


def spam(n=42):
spud = Potato()
return spud.foo(n=n)


class Potato(object):


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


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


class PotatoTest(unittest.TestCase):


def test_something(self):
foo = spy_decorator(Potato.foo)
with unittest.mock.patch.object(Potato, 'foo', foo):
forty_two = spam(n=40)
foo.mock.assert_called_once_with(n=40)
self.assertEqual(forty_two, 42)




if __name__ == '__main__':
unittest.main()

If the method replaced accepts mutable arguments which are modified under test, you might wish to initialize a CopyingMock* in place of the MagicMock inside the spy_decorator.

*It's a recipe taken from the docs which I've published on PyPI as copyingmock lib

For those who don't mind using side_effect, here's a solution with a few pros:

  • Uses decorator syntax
  • Patches an unbound method, which I find more versatile
    • Requires inclusion of the instance in the assertion
class PotatoTest(TestCase):


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

You are describing the same question than Python mock: wrap instance method. My solution in https://stackoverflow.com/a/72446339/9230828 can be applied as following: Put wrap_object somewhere, e.g. into 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:
spud = Potato()
forty_two = spud.foo(n=40)
mock.assert_called_once_with(n=40)
self.assertEqual(forty_two, 42)

I did it with a bit another way because IMO mocking is preferable over patching

from unittest.mock import create_autospec




mocked_method = create_autospec(
spec=my_method,
spec_set=True,
# Will implement a real behavior rather than return a Mock instance
side_effect=*a, **kw: my_method.do_something(*a, **kw))
mocked_object.do_something()
mocked_object.assert_called_once()