Python,模拟和包装方法而不实例化对象

问题描述 投票:0回答:4

我想模拟类的方法并使用

wraps
,以便实际调用它,但我可以检查传递给它的参数。我在几个地方(例如这里)看到通常的方法如下(经过调整以表明我的观点):

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)

但是,这会实例化类

Potato
,以便将模拟绑定到实例方法
spud.foo

我需要的是在

foo
的所有实例中模拟方法
Potato
,并将它们包装在原始方法周围。即,我需要以下内容:

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)

这当然行不通。我收到错误:

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

但是,如果不使用

wraps

,它就可以工作,所以问题不在于模拟本身,而在于它调用包装函数的方式。例如,这是可行的(但当然我必须“伪造”返回值,因为现在 
Potato.foo
 永远不会真正运行):

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)

这可行,但它不运行原始函数,我需要运行该函数,因为返回值在其他地方使用(并且我无法从测试中伪造它)。

可以做吗?

注意 我的需求背后的实际原因是我正在使用 webtest 测试rest api。在测试中,我对某些路径执行了一些 wsgi 请求,并且我的框架实例化了一些类并使用它们的方法来满足请求。我想捕获发送到这些方法的参数,以便在我的测试中对它们进行一些asserts

python-3.x unit-testing mocking
4个回答
6
投票
简而言之,您无法单独使用

Mock

 实例来做到这一点。 

patch.object

 为指定实例(Potato)创建 
Mock
,即在调用时用单个 Mock 替换 
Potato.foo
。因此,无法将实例传递给 
Mock
,因为模拟是在任何实例之前创建的。据我所知,在运行时获取实例信息到 
Mock
 也非常困难。

举例说明:

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

输出为:

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

我建议对你的类进行猴子修补,以便将

Mock

 添加到正确的位置。 

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

请注意,当您使用实例调用函数时,使用

called_with

 是有问题的。 


1
投票
你的问题在我看来与

python模拟 - 修补方法而不妨碍实现相同。 https://stackoverflow.com/a/72446739/9230828 实现你想要的(除了它使用 with 语句而不是装饰器)。 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: self.spud = Potato() forty_two = self.spud.foo(n=40) mock.assert_called_once_with(n=40) self.assertEqual(forty_two, 42)
    

0
投票
您是否控制

Potato

 实例的创建,或者至少在创建这些实例后有权访问它们?你应该这样做,否则你将无法检查特定的参数列表。 
如果是这样,您可以使用
包装单个实例的方法

spud = dig_out_a_potato() with mock.patch.object(spud, "foo", wraps=spud.foo) as mock_spud: # do your thing. mock_spud.assert_called...
    

0
投票
我认为这个问题有一个更简单的解决方案。由于unittest.mock无法原生处理类方法,因此您可以在此处创建一个部分,传入您事先创建的

Potato

实例,这为您提供了您正在寻找的控件:

from unittest import TestCase from unittest.mock import patch from functools import partial 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=partial(Potato.foo, spud)) def test_something(self, mock): forty_two = self.spud.foo(n=40) mock.assert_called_once_with(n=40) self.assertEqual(forty_two, 42)
这会模拟 

Potatoall

 实例,同时保持 
wraps
 的使用
    

© www.soinside.com 2019 - 2024. All rights reserved.