python mock -在不妨碍实现的情况下修补方法

w1jd8yoj  于 12个月前  发布在  Python
关注(0)|答案(6)|浏览(78)

有没有一种干净的方法来修补一个对象,以便在测试用例中获得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来解决这个问题,但我希望有一种更好的方法,可以在所有函数,类方法,静态方法,未绑定方法等上以相同的方式工作。

b91juud3

b91juud31#

与您的解决方案类似,但使用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)

根据the documentation

  • wraps*:要 Package 的模拟对象的项。如果wraps不是None,那么调用Mock将把调用传递给被 Package 的对象(返回真实的结果)。对mock的属性访问将返回一个Mock对象,该对象 Package 了 Package 对象的相应属性(因此尝试访问不存在的属性将引发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)
1szpjjfi

1szpjjfi2#

这个答案解决了用户Quuxplusone的赏金中提到的额外要求:
对于我的用例来说,重要的是它可以与@patch.mock一起工作,即在构造Potato示例(本例中为spud)和调用spud.foo之间不需要插入任何代码。我需要从一开始就使用mocked-out foo方法创建spud,因为我不控制创建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()

如果replaced方法接受在测试中修改的可变参数,您可能希望初始化一个CopyingMock * 来代替spy_decorator中的MagicMock

  • 这是一个从我在PyPI上发布的文档中提取的配方,作为copyingmock lib
rjee0c15

rjee0c153#

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

  • 使用装饰器语法
  • 修补一个未绑定的方法,我发现它更通用
  • 要求在Assert中包含示例
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)
aij0ehis

aij0ehis4#

您正在描述与Python mock: wrap instance method相同的问题。我在https://stackoverflow.com/a/72446339/9230828中的解决方案可以应用如下:把wrap_object放在某个地方,例如wrap_object.py

# Copyright (C) 2022, Benjamin Drung <[email protected]>
#
# 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)
m3eecexj

m3eecexj5#

我做了一个有点另一种方式,因为海事组织嘲笑是更可取的修补

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

uqjltbpv6#

这似乎也适用于mock.patch
假设我有这个模块my_mod.py

def my_func(blah, yo):
    return yo, blah

def main(*args, **kwargs):
    return my_func(*args, **kwargs)

我想验证my_func是否被调用以及它的调用方式,但我不想更改它的行为。
我可以在模拟中使用 Package 。补丁似乎。

def test_my_func():
    from tmp.my_mod import main, my_func

    with mock.patch("tmp.my_mod.my_func", wraps=my_func) as mock_func:
        assert main("1", "2") == ("2", "1")
    assert mock_func.call_args_list[0] == (("1", "2"), {})

相关问题