在Python类的任何实例上模拟方法

60 投票
6 回答
73836 浏览
提问于 2025-04-16 12:02

我想在生产代码中的某个类的实例上模拟(mock)一些方法,以便于测试。请问在Python中有没有什么库可以帮助我做到这一点?

基本上,我想做的事情是这样的,但我需要用Python来实现(下面的代码是Ruby,使用的是Mocha库):

  def test_stubbing_an_instance_method_on_all_instances_of_a_class
    Product.any_instance.stubs(:name).returns('stubbed_name')
    assert_equal 'stubbed_name', SomeClassThatUsesProduct.get_new_product_name
  end

这里需要注意的是,我需要在类的层面上进行模拟,因为我实际上是需要在我正在测试的东西创建的实例上模拟方法。

使用场景:

我有一个类叫做 QueryMaker,它会调用一个 RemoteAPI 实例上的方法。我想模拟 RemoteAPI.get_data_from_remote_server 这个方法,让它返回一个常量。请问我该如何在测试中做到这一点,而不需要在 RemoteAPI 的代码里添加特殊的情况来检查它运行的环境。

以下是我想要的效果示例:

# a.py
class A(object):
    def foo(self):
        return "A's foo"

# b.py
from a import A

class B(object):
    def bar(self):
        x = A()
        return x.foo()

# test.py
from a import A
from b import B

def new_foo(self):
    return "New foo"

A.foo = new_foo

y = B()
if y.bar() == "New foo":
    print "Success!"

6 个回答

7

模拟(Mock)就是这样做的,没错。确保你在任何从这个类创建的实例上正确地修改实例方法可能有点棘手。

# a.py
class A(object):
    def foo(self):
        return "A's foo"

# b.py
from a import A

class B(object):
    def bar(self):
        x = A()
        return x.foo()

# test.py
from a import A
from b import B
import mock

mocked_a_class = mock.Mock()
mocked_a_instance = mocked_a_class.return_value
mocked_a_instance.foo.return_value = 'New foo'

with mock.patch('b.A', mocked_a_class):  # Note b.A not a.A
    y = B()
    if y.bar() == "New foo":
        print "Success!"

文档中提到,关于“要配置被修改类的实例方法的返回值...”的段落。

83

在测试的时候,需要模拟一些方法是很常见的事情,Python里有很多工具可以帮助你做到这一点。不过,使用“猴子补丁”这种方式来修改类是有风险的,因为如果你不在之后把它恢复,那么这个类在你测试的其他地方也会被修改。

我使用的库叫做mock,它是最受欢迎的Python模拟库之一,里面有一个叫“patch”的助手,可以帮助你在测试中安全地修改对象和类的方法或属性。

你可以在这里找到mock模块:

http://pypi.python.org/pypi/mock

这个patch装饰器可以作为上下文管理器使用,也可以作为测试装饰器使用。你可以选择自己用函数来修改,或者使用可以高度配置的Mock对象来自动进行修改。

from a import A
from b import B

from mock import patch

def new_foo(self):
    return "New foo"

with patch.object(A, 'foo', new_foo):
    y = B()
    if y.bar() == "New foo":
        print "Success!"

这样它会自动帮你处理恢复的工作。你甚至可以不自己定义模拟函数:

from mock import patch

with patch.object(A, 'foo') as mock_foo:
    mock_foo.return_value = "New Foo"

    y = B()
    if y.bar() == "New foo":
        print "Success!"
4

最简单的方法可能是使用类方法。其实你应该使用实例方法,但创建实例方法比较麻烦,而类方法有现成的函数可以用。使用类方法时,你的占位符会把类本身作为第一个参数传进去,而不是实例,不过因为这是个占位符,这点可能没什么关系。所以:

Product.name = classmethod(lambda cls: "stubbed_name")

注意,lambda的签名(也就是它的参数和返回值的格式)必须和你要替换的方法的签名一致。当然,由于Python(和Ruby一样)是动态语言,所以没有办法保证在你拿到实例之前,别人不会把你占位的方法换成其他的东西,不过我想如果真的发生这种情况,你很快就会发现的。

编辑:经过进一步调查,你可以不使用classmethod()

Product.name = lambda self: "stubbed_name"

我原本想尽量保持原方法的行为不变,但看起来其实并不是必须的(而且也没有保持我希望的那种行为)。

撰写回答