python mock - 不干扰实现的方法打补丁

107 投票
6 回答
56783 浏览
提问于 2025-04-19 05:36

有没有一种简单的方法可以对一个对象进行补丁,这样在你的测试案例中就能使用到 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 来搞定这个,但我希望能有一种更好的方法,能够在所有函数、类方法、静态方法和未绑定的方法上都能一样好用。

6 个回答

0

我用了一种稍微不同的方法,因为在我看来,模拟比修改更好。

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()
1

你提到的问题和这个Python mock: wrap instance method的内容是一样的。我在这个链接中给出的解决方案可以这样应用:把wrap_object放到某个地方,比如放在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

然后你可以写下面的单元测试:

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)
12

对于那些不介意使用 side_effect 的人,这里有一个解决方案,优点有几个:

  • 使用了装饰器的语法
  • 修补了一个未绑定的方法,我觉得这样更灵活
    • 在断言中需要包含实例
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)
19

这个回答是针对用户Quuxplusone在悬赏中提到的额外需求的。

对我来说,最重要的是它能与@patch.mock一起使用,也就是说,我不需要在创建Potato实例(在这个例子中是spud)和调用spud.foo之间插入任何代码。我需要spud一开始就用一个模拟的foo方法来创建,因为我无法控制spud被创建的地方。

上面描述的用例可以通过使用装饰器来轻松实现:

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()

如果被替换的方法接受可变参数,并且这些参数在测试中会被修改,你可能希望在spy_decorator中用CopyingMock替代MagicMock

*这是一个来自于文档的示例,我已经将其发布为copyingmock库。

91

这个方案和你的类似,但使用了 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)

根据文档的说明:

wraps:这是一个用于模拟对象的包装项。如果 wraps 不是 None,那么调用这个模拟对象时,会把调用传递给被包装的对象(并返回真实的结果)。在模拟对象上访问属性时,会返回一个模拟对象,这个模拟对象会包装被包装对象的相应属性(所以如果你尝试访问一个不存在的属性,就会抛出一个 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)

撰写回答