【问题标题】:How to mock Python static methods and class methods如何模拟 Python 静态方法和类方法
【发布时间】:2014-04-24 00:07:47
【问题描述】:

如何模拟具有未绑定方法的类?比如这个类有一个@classmethod和一个@staticmethod:

class Calculator(object):
    def __init__(self, multiplier):
        self._multiplier = multiplier
    def multiply(self, n):
        return self._multiplier * n
    @classmethod
    def increment(cls, n):
        return n + 1
    @staticmethod
    def decrement(n):
        return n - 1

calculator = Calculator(2)
assert calculator.multiply(3) == 6    
assert calculator.increment(3) == 4
assert calculator.decrement(3) == 2
assert Calculator.increment(3) == 4
assert Calculator.decrement(3) == 2

上面几乎描述了我的问题。以下是一个工作示例,演示了我尝试过的事情。

类Machine 包含Calculator 的一个实例。我将使用Calculator 的模拟来测试Machine。为了演示我的问题,Machine 通过Calculator 的实例和Calculator 类调用未绑定的方法:

class Machine(object):
    def __init__(self, calculator):
        self._calculator = calculator
    def mult(self, n):
        return self._calculator.multiply(n)
    def incr_bound(self, n):
        return self._calculator.increment(n)
    def decr_bound(self, n):
        return self._calculator.decrement(n)
    def incr_unbound(self, n):
        return Calculator.increment(n)
    def decr_unbound(self, n):
        return Calculator.decrement(n)

machine = Machine(Calculator(3))
assert machine.mult(3) == 9

assert machine.incr_bound(3) == 4
assert machine.incr_unbound(3) == 4

assert machine.decr_bound(3) == 2
assert machine.decr_unbound(3) == 2

上面的所有功能代码都可以正常工作。接下来是不起作用的部分。

我创建了一个模拟 Calculator 用于测试 Machine:

from mock import Mock

def MockCalculator(multiplier):
    mock = Mock(spec=Calculator, name='MockCalculator')

    def multiply_proxy(n):
        '''Multiply by 2*multiplier instead so we can see the difference'''
        return 2 * multiplier * n
    mock.multiply = multiply_proxy

    def increment_proxy(n):
        '''Increment by 2 instead of 1 so we can see the difference'''
        return n + 2
    mock.increment = increment_proxy

    def decrement_proxy(n):
        '''Decrement by 2 instead of 1 so we can see the difference'''
        return n - 2
    mock.decrement = decrement_proxy

    return mock

在下面的单元测试中,绑定方法使用MockCalculator,正如我所希望的那样。但是,对Calculator.increment() 和Calculator.decrement() 的调用仍然使用Calculator:

import unittest

class TestMachine(unittest.TestCase):
    def test_bound(self):
        '''The bound methods of Calculator are replaced with MockCalculator'''
        machine = Machine(MockCalculator(3))
        self.assertEqual(machine.mult(3), 18)
        self.assertEqual(machine.incr_bound(3), 5)
        self.assertEqual(machine.decr_bound(3), 1)

    def test_unbound(self):
        '''Machine.incr_unbound() and Machine.decr_unbound() are still using
        Calculator.increment() and Calculator.decrement(n), which is wrong.
        '''
        machine = Machine(MockCalculator(3))
        self.assertEqual(machine.incr_unbound(3), 4)    # I wish this was 5
        self.assertEqual(machine.decr_unbound(3), 2)    # I wish this was 1

所以我尝试修补Calculator.increment() 和Calculator.decrement():

def MockCalculatorImproved(multiplier):
    mock = Mock(spec=Calculator, name='MockCalculatorImproved')

    def multiply_proxy(n):
        '''Multiply by 2*multiplier instead of multiplier so we can see the difference'''
        return 2 * multiplier * n
    mock.multiply = multiply_proxy
    return mock

def increment_proxy(n):
    '''Increment by 2 instead of 1 so we can see the difference'''
    return n + 2

def decrement_proxy(n):
    '''Decrement by 2 instead of 1 so we can see the difference'''
    return n - 2


from mock import patch

@patch.object(Calculator, 'increment', increment_proxy)
@patch.object(Calculator, 'decrement', decrement_proxy)
class TestMachineImproved(unittest.TestCase):
    def test_bound(self):
        '''The bound methods of Calculator are replaced with MockCalculator'''
        machine = Machine(MockCalculatorImproved(3))
        self.assertEqual(machine.mult(3), 18)
        self.assertEqual(machine.incr_bound(3), 5)
        self.assertEqual(machine.decr_bound(3), 1)

    def test_unbound(self):
        '''machine.incr_unbound() and Machine.decr_unbound() should use
        increment_proxy() and decrement_proxy(n).
        '''
        machine = Machine(MockCalculatorImproved(3))
        self.assertEqual(machine.incr_unbound(3), 5)
        self.assertEqual(machine.decr_unbound(3), 1)

即使在修补之后,未绑定的方法仍需要 Calculator 的实例作为参数:

TypeError: unbound method increment_proxy() 必须以 Calculator 实例作为第一个参数调用(取而代之的是 int 实例)

如何模拟类方法Calculator.increment() 和静态方法Calculator.decrement()?

【问题讨论】:

    标签: python unit-testing mocking


    【解决方案1】:

    你修补了错误的对象。您必须修补来自 Machine 类的 Calculator,而不是一般的 Calculator 类。阅读它here。

    from mock import patch
    import unittest
    
    from calculator import Calculator
    from machine import Machine
    
    
    class TestMachine(unittest.TestCase):
        def my_mocked_mult(self, multiplier):
            return 2 * multiplier * 3
        def test_bound(self):
            '''The bound methods of Calculator are replaced with MockCalculator'''
            machine = Machine(Calculator(3))
            with patch.object(machine, "mult") as mocked_mult:
                mocked_mult.side_effect = self.my_mocked_mult
                self.assertEqual(machine.mult(3), 18)
                self.assertEqual(machine.incr_bound(3), 5)
                self.assertEqual(machine.decr_bound(3), 1)
    
        def test_unbound(self):
            '''Machine.incr_unbound() and Machine.decr_unbound() are still using
            Calculator.increment() and Calculator.decrement(n), which is wrong.
            '''
            machine = Machine(Calculator(3))
            self.assertEqual(machine.incr_unbound(3), 4)    # I wish this was 5
            self.assertEqual(machine.decr_unbound(3), 2)    # I wish this was 1
    

    【讨论】:

    • 感谢您的回复。我正在测试 Machine 类,因此修补 Machine.mult() 之类的方法并不令人满意。此外,模拟 MockComputer.multiplier() 工作正常。我的问题是关于模拟或修补静态和类方法 Computer.increment() 和 Computer.decrement()。
    【解决方案2】:

    一种方法是

    def test_increment(mocker):
        mocker.patch.object(Calculator, attribute='increment', return_value=10)
        ...actual test code...
    

    【讨论】:

    • "mocker"???你必须解释这是什么!我用谷歌搜索并假设它来自我安装的“pytest-mock”。我做了一条像你这样的行,它的失败方式与mock 上下文管理器失败的方式完全相同。然后我尝试了mocker.patch,后跟一个字符串,在我的例子中是“app.App.exit”(QApplication 的子类)。有效。其他人注意:这不是上下文管理器:末尾没有冒号,下一行没有缩进。有用的魔法,谢谢!
    【解决方案3】:

    我刚刚做了一些可以转化为您的案例的事情,如下所示:

    class Calculator_Mock(object):
        def __init__(self, multiplier):
            ... # add whatever you need here
    
        def multiply(self, n):
            ... # add whatever you need here
    
        @classmethod
        def increment(self, n):
            ... # add whatever you need here
    

    然后,在您的测试中,像这样简单:

    class TestCalculator(TestCase):
    
        def test_increment_or_whatever(self):
            with patch.object(Calculator,
                              "increment",
                              return_value=Calculator_Mock.increment()) as increment_mock:
            ... # call whatever your calls Calculator.increment, the mock should run instead the Calculator.increment
    

    【讨论】:

    【解决方案4】:

    C#、Java 和 C++ 程序员倾向于在 Python 中过度使用类和静态方法。 Pythonic 方法是使用模块函数。

    首先,这里是重构的待测软件,方法increment() 和decrement() 作为模块函数。界面确实发生了变化,但功能是一样的:

    # Module machines
    
    class Calculator(object):
        def __init__(self, multiplier):
            self._multiplier = multiplier
        def multiply(self, n):
            return self._multiplier * n
    
    def increment(n):
        return n + 1
    
    def decrement(n):
        return n - 1
    
    calculator = Calculator(2)
    assert calculator.multiply(3) == 6
    assert increment(3) == 4
    assert decrement(3) == 2
    
    
    class Machine(object):
        '''A larger machine that has a calculator.'''
        def __init__(self, calculator):
            self._calculator = calculator
        def mult(self, n):
            return self._calculator.multiply(n)
        def incr(self, n):
            return increment(n)
        def decr(self, n):
            return decrement(n)
    
    machine = Machine(Calculator(3))
    assert machine.mult(3) == 9
    assert machine.incr(3) == 4
    assert machine.decr(3) == 2
    

    添加函数increment_mock()和decrement_mock()来模拟increment()和decrement():

    from mock import Mock
    import machines
    
    def MockCalculator(multiplier):
        mock = Mock(spec=machines.Calculator, name='MockCalculator')
    
        def multiply_proxy(n):
            '''Multiply by 2*multiplier instead of multiplier so we can see the
            difference.
            '''
            return 2 * multiplier * n
        mock.multiply = multiply_proxy
    
        return mock
    
    def increment_mock(n):
        '''Increment by 2 instead of 1 so we can see the difference.'''
        return n + 2
    
    def decrement_mock(n):
        '''Decrement by 2 instead of 1 so we can see the difference.'''
        return n - 2
    

    现在是好的部分。修补 increment() 和 decrement() 以用他们的模拟替换它们:

    import unittest
    from mock import patch
    import machines
    
    @patch('machines.increment', increment_mock)
    @patch('machines.decrement', decrement_mock)
    class TestMachine(unittest.TestCase):
        def test_mult(self):
            '''The bound method of Calculator is replaced with MockCalculator'''
            machine = machines.Machine(MockCalculator(3))
            self.assertEqual(machine.mult(3), 18)
    
        def test_incr(self):
            '''increment() is replaced with increment_mock()'''
            machine = machines.Machine(MockCalculator(3))
            self.assertEqual(machine.incr(3), 5)
    
        def test_decr(self):
            '''decrement() is replaced with decrement_mock()'''
            machine = machines.Machine(MockCalculator(3))
            self.assertEqual(machine.decr(3), 1)
    

    【讨论】:

    • 这是正确答案。 [关于另一个问题]静态方法与模块方法的问题](programmers.stackexchange.com/questions/112137/…) 进行了讨论,结论是静态方法是一种代码味道,是对 Java 风格的模仿,其中不存在模块函数定义,而静态方法是唯一的替代品。
    • 这不能回答问题。 staticmethod 是一个有效的 python 构造,知道如何模拟这些函数很有价值。 “做其他事情”不是正确的答案,尤其是考虑到您可以模拟静态方法。
    • 阅读本文并意识到我也过度使用了静态方法,而我应该使用模块函数。尽管这似乎与问题无关,但它确实帮助了我,谢谢。
    • ... Java 开发人员与 Python 中过度使用静态方法有什么关系?体面的 Java 开发人员都知道不要过度使用静态方法,因为它很难测试。我个人主要将它用于有用的简单逻辑代码,这些代码可以重用很多时间,然后你可以将它们分组到一些实用程序、帮助程序类中。我很少看到我需要模拟静态代码,因为它应该很简单并且可以根据它们的逻辑进行测试。所有类调用静态代码,需要在调用前后测试它们的输出,而不是模拟静态代码本身。
    • 我知道这很旧,但是当静态方法返回实例或获取实例时,它们不是“代码味道”(我讨厌的短语,抱歉),所以请具体说明,以便初级程序员不要不要误入歧途。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-14
    相关资源
    最近更新 更多