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

Joh*_*hee 10 python unit-testing mocking

如何模拟具有未绑定方法的类?例如,这个类有一个@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
Run Code Online (Sandbox Code Playgroud)

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

Machine包含一个实例Calculator.我将Machine用模拟测试Calculator.要演示我的问题,请Machine通过类的实例CalculatorCalculator类来调用未绑定的方法:

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
Run Code Online (Sandbox Code Playgroud)

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

我创建了一个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
Run Code Online (Sandbox Code Playgroud)

在下面的单元测试中,绑定的方法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
Run Code Online (Sandbox Code Playgroud)

所以我尝试修补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)
Run Code Online (Sandbox Code Playgroud)

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

TypeError:必须使用Calculator实例作为第一个参数调用unbound方法increment_proxy()(改为使用int实例)

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

Ale*_*uie 7

你正在修补错误的对象.你必须修补CalculatorMachine类,而不是一般的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
Run Code Online (Sandbox Code Playgroud)

  • 感谢您的答复。我正在测试 Machine 类,因此对 Machine.mult() 这样的修补方法并不令人满意。此外,模拟 MockComputer.multiplier() 工作正常。我的问题是关于模拟或修补静态和类方法 Computer.increment() 和 Computer.decrement()。 (2认同)

Joh*_*hee -5

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
Run Code Online (Sandbox Code Playgroud)

添加函数increment_mock()anddecrement_mock()来模拟increment()and 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
Run Code Online (Sandbox Code Playgroud)

现在来说说好的部分。修补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)
Run Code Online (Sandbox Code Playgroud)

  • 这并不能回答问题。`staticmethod` 是一个有效的 python 构造,了解如何模拟这些函数是很有价值的。“做其他事情”不是正确的答案,特别是考虑到您可以模拟静态方法。 (51认同)
  • 这是正确的答案。[关于另一个问题]静态方法与模块方法问题](http://programmers.stackexchange.com/questions/112137/is-staticmethod-proliferation-a-code-smell)进行了讨论,结论是静态方法是代码味道和 Java 风格的模仿,其中不存在模块函数定义,静态方法是唯一的替代品。 (4认同)