如何模拟Python静态方法和类方法

20

如何模拟一个具有未绑定方法的类?例如,此类具有 @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: 未绑定的方法increment_proxy()必须使用Calculator实例作为第一个参数调用(而不是int实例)

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

4个回答

14
你补丁的对象不正确。你必须修补来自Machine类的Calculator,而不是一般的Calculator类。在这里阅读更多相关信息
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

2
谢谢您的回复。我正在测试Machine类,因此补丁方法如Machine.mult()并不令人满意。此外,模拟MockComputer.multiplier()也可以正常工作。我的问题是关于模拟或补丁静态和类方法Computer.increment()和Computer.decrement()。 - John McGehee

4

一种方法是

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

4
mocker”是什么?你需要解释清楚!我谷歌搜索了一下,猜测它来源于“pytest-mock”,我已经安装了它。我尝试了和你的代码行一样的方法,但它失败了,和使用mock上下文管理器失败的情况一模一样。然后我尝试使用mocker.patch,后面跟着一个字符串,在我的例子中是“app.App.exit”(继承自QApplication)。这个方法起作用了。提醒其他人注意:这不是一个上下文管理器:没有冒号在结尾,并且下一行也没有缩进。这是一个有用的神奇方法,所以谢谢! - mike rodent

1

我刚刚做了一件类似于你情况的事情,可以这样翻译:

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

1
这个很好用。Py3文档:https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.return_value - kevinarpe

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

4
这篇文章讨论了静态方法和模块方法的问题,并得出结论:静态方法是代码异味,是对Java风格的模仿,在那里模块函数定义不存在,静态方法是唯一的替代品。 - user9903
48
这并没有回答问题。staticmethod是Python中有效的结构,了解如何mock这些函数很有价值。“做别的事情”不是正确的答案,特别是考虑到您可以mock静态方法。 - AnilRedshift
1
阅读这篇文章后,我意识到我在许多情况下过度使用了静态方法,而本应该使用模块函数。即使这似乎与问题无关,但它确实帮了我大忙,谢谢。 - Jeff Siver
Java开发人员与Python中静态方法的过度使用有什么关系?优秀的Java开发人员都知道不要过度使用静态方法,因为这很难测试。我个人大多数情况下只使用它来编写有用的简单逻辑代码,这些代码会被多次重复使用,然后您可以将它们分组到一些实用程序、帮助类中。我很少看到需要模拟静态代码,因为它应该是简单的并且已经在其逻辑上进行了测试。所有调用静态代码的类,在调用之前和之后都需要测试其输出,而不是模拟静态代码本身。 - sernle
我知道这已经很老了,但静态方法并不是“代码异味”(我很讨厌这个词,抱歉),当它们返回一个实例或者接受一个实例时,请具体说明,以免误导初级程序员。 - WattsInABox

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接