模拟python类的任何实例上的方法

时间:2011-02-18 02:00:48

标签: python testing mocking

我想在生产代码中模拟某些类的任何实例上的方法,以便于测试。 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!"

5 个答案:

答案 0 :(得分:47)

在测试时需要模拟方法很常见,并且有很多工具可以帮助你在Python中使用它。像这样的“猴子修补”类的危险在于,如果之后撤消,那么在整个测试过程中,类已被修改用于所有其他用途。

我的库mock是最受欢迎的Python模拟库之一,它包含一个名为“patch”的帮助程序,可帮助您在测试期间安全地修补对象和类的方法或属性。

模拟模块可从以下网址获得:

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

补丁装饰器可以用作上下文管理器或测试装饰器。您既可以使用它自己修补功能,也可以使用它自动修补可配置的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!"

答案 1 :(得分:2)

最简单的方法可能是使用类方法。你真的应该使用一个实例方法,但是创建它们很痛苦,而有一个内置函数可以创建一个类方法。使用类方法,您的存根将获得对类(而不是实例)的引用作为第一个参数,但由于它是存根,这可能无关紧要。所以:

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

请注意,lambda的签名必须与您要替换的方法的签名匹配。当然,由于Python(如Ruby)是一种动态语言,因此无法保证在你开始实际操作之前,有人不会为你的其他东西切换你的存根方法,尽管我希望你能很快知道如果发生这种情况。

编辑:进一步调查后,您可以省略classmethod()

Product.name = lambda self: "stubbed_name"

我试图尽可能地保留原始方法的行为,但看起来它实际上并不是必需的(并且不会保留我希望的行为,无论如何)。

答案 2 :(得分:1)

我不太了解Ruby,无法确切地告诉您要做什么,但请查看__getattr__方法。如果您在类中定义它,Python将在代码尝试访问未定义的类的任何属性时调用它。由于您希望它是一个方法,因此需要动态创建它返回的方法。

>>> class Product:
...     def __init__(self, number):
...         self.number = number
...     def get_number(self):
...         print "My number is %d" % self.number
...     def __getattr__(self, attr_name):   
...         return lambda:"stubbed_"+attr_name
... 
>>> p = Product(172)
>>> p.number
172
>>> p.name()
'stubbed_name'
>>> p.get_number()
My number is 172
>>> p.other_method()
'stubbed_other_method'

另请注意,__getattr__需要使用您的类的任何其他未定义属性,否则它将无限递归,调用__getattr__以获取不属于该属性的属性存在。

...     def __getattr__(self, attr_name):   
...         return self.x
>>> p.y
Traceback (most recent call last):
#clipped
RuntimeError: maximum recursion depth exceeded while calling a Python object

如果您只想从测试代码而不是生产代码那里做,那么将您的普通类定义放在生产代码文件中,然后在测试代码中定义__getattr__方法(未绑定) ,然后将其绑定到您想要的类:

#production code
>>> class Product:
...     def __init__(self, number):
...         self.number = number
...     def get_number(self):
...         print "My number is %d" % self.number
...         

#test code
>>> def __getattr__(self, attr):
...     return lambda:"stubbed_"+attr_name
... 
>>> p = Product(172)
>>> p.number
172
>>> p.name()
Traceback (most recent call last):
  File "<interactive input>", line 1, in <module>
AttributeError: Product instance has no attribute 'name'
>>> Product.__getattr__ = __getattr__
>>> p.name()
'stubbed_name'

我不确定这会如何对已使用__getattribute__的类做出反应(与__getattr__相反,__getattribute__会为所有属性调用,无论它们是否存在)

如果您只想对已经存在的特定方法执行此操作,那么您可以执行以下操作:

#production code
>>> class Product:
...     def __init__(self, number):
...         self.number = number
...     def get_number(self):
...         return self.number
...     
>>> p = Product(172)
>>> p.get_number()
172

#test code
>>> def get_number(self):
...     return "stub_get_number"
... 
>>> Product.get_number = get_number
>>> p.get_number()
'stub_get_number'

或者如果你真的想要优雅,你可以创建一个包装函数来简化多种方法:

#test code
>>> import functools
>>> def stubber(fn):
...     return functools.wraps(fn)(lambda self:"stub_"+fn.__name__)
... 
>>> Product.get_number = stubber(Product.get_number)
>>> p.get_number()
'stub_get_number'

答案 3 :(得分:1)

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!"

docs中引用,在para开始&#34;为补丁类上的实例方法配置返回值...&#34;

答案 4 :(得分:0)

#Orignal Class definition - path "module.Product"

class Product:

    def method_A(self):
        # do something
        pass

    def method_B(self):
        self.random_attr = 1



#Test case

from module import Product
class MockedProduct(Product):
    
    def method_B(self):
        self.random_attr = 2


with mock.patch('module.Product', new=MockedProduct):
    #Write test case logic here
    #Now method_B function call on product class instance should return 2 
    #instead of 1