【问题标题】:Mocking out methods on any instance of a python class在 python 类的任何实例上模拟方法
【发布时间】:2011-06-29 13:05:00
【问题描述】:

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

【问题讨论】:

    标签: python testing mocking


    【解决方案1】:

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

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

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

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

    Product.name = lambda self: "stubbed_name"
    

    我试图尽可能地保留原始方法的行为,但它看起来实际上并没有必要(无论如何也没有像我希望的那样保留行为)。

    【讨论】:

    • 我并没有完全理解它是如何工作的。这是否满足我的用例?
    • 应该;您的用例与您的示例几乎相同。基本上,通过更改类上方法的定义,您也在更改所有实例上的定义。 classmethod 函数创建了一个包装器,基本上将函数转换为方法,lambda 将函数定义为简单地返回一个静态字符串。
    • 好的 - 我必须对此进行试验,但我对导入时的设置方式有点模糊。如果我导入RemoteAPI,然后替换其中一种方法,那么QueryMaker 在正常执行期间导入RemoteAPIRemoteAPI 使用哪个版本的QueryMaker?我的版本还是原版?
    • 只有一个版本。只有第一个 import 导入任何东西;其余的只是获取对已加载模块对象的引用。
    • 另请注意,在重新定义 Product.name 之前创建的任何 Product 实例也将被“更新”以拥有新方法,这可能是您想要的。此外,此方法不会保留原始函数的名称和文档字符串。
    【解决方案2】:

    我不太了解 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'
    

    【讨论】:

    • 本练习的目标是避免仅仅为了能够测试而修改任何生产代码。我很确定你的建议并没有做到这一点。
    • 如果这是你想做的,我很确定你可以在你的测试代码中将它定义为一个未绑定的函数,然后通过Product.__getattr__ = fn_name将它绑定到你的测试代码中的类跨度>
    • 我了解__getattr__ 的用途,但我真的不明白您为什么认为它会有用。我需要替换在我需要模拟该方法的类上已经定义的方法。
    • @Jamie 检查我答案中的最后两个示例,编辑显示如何替换特定方法。就像我说的,我并不真正了解 Ruby,所以当您提到模拟方法和创建存根时,我以为您正在尝试为所有方法创建测试存根以方便编写单元测试。我现在看到它是您要替换的特定方法,因此最后两个示例应该说明如何做到这一点。
    【解决方案3】:

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

    我的模拟库是最受欢迎的 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!"
    

    【讨论】:

    • 啊-谢谢。我查看了mock,但我没有看到任何明显的例子,它用于修补在其他地方导入的东西。 +1
    • 在你的测试中直接使用猴子补丁要小心,它很容易破坏东西。 mock.patch 可用于修补任何命名空间中的内容 - 当前命名空间中的对象(使用 patch.object)或名称指定的另一个命名空间中的对象:patch('mymodule.myclass')
    • 要知道的重要一点是,如果您按名称进行修补,那么您将在它使用的模块中修补名称,而不是在它定义的模块中。例如如果我想在模块foo 中修补SomeClass 的使用,即使SomeClass 在模块bar 中定义但被导入foo,那么你修补的是foo.SomeClass不是 bar.SomeClass。对于修补类方法来说,这并不重要,因为类 object 在两个模块中都是同一个对象。对于其他事情,这非常重要。
    • 测试用例不应该调用 Y.foo() == "New foo" 吗?这是 patch.object 的模拟文档中的示例:@patch.object(SomeClass, 'class_method')
    • 这是我遇到过的最令人困惑的代码示例。 B == A 和 foo == bar 是不可能的。然而,在这个例子中,两者可以互换使用。
    【解决方案4】:

    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 中引用,在“在修补类上配置实例方法的返回值...”开头的段落中

    【讨论】:

      【解决方案5】:
      #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 
      

      【讨论】:

        猜你喜欢
        • 2020-06-17
        • 1970-01-01
        • 1970-01-01
        • 2020-04-30
        • 1970-01-01
        • 1970-01-01
        • 2021-11-02
        相关资源
        最近更新 更多