John McGehee John McGehee - 4 months ago 14
Python Question

How to mock Python static methods and class methods

How do I mock a class that has unbound methods? For example, this class has a

@classmethod
and a
@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


The above pretty much describes my question. The following is a working example that demonstrates the things I have tried.

Class
Machine
contains an instance of
Calculator
. I will be testing
Machine
with a mock of
Calculator
. To demonstrate my issue,
Machine
calls the unbound methods via an instance of
Calculator
and via the
Calculator
class:

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


All the functional code above works fine. Next is the part that does not work.

I create a mock of
Calculator
to use in testing
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


In the unit test below, the bound methods use
MockCalculator
as I had hoped. However, the calls to
Calculator.increment()
and
Calculator.decrement()
still use
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


So I try to patch
Calculator.increment()
and
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)


Even after patching, the unbound methods want an instance of
Calculator
as an argument:


TypeError: unbound method increment_proxy() must be called with Calculator instance as first argument (got int instance instead)


How do I mock out class method
Calculator.increment()
and static method
Calculator.decrement()
?

Answer

The solution is to use module functions instead. Module functions are more Pythonic, anyway. My overuse of class and static methods was influenced by past experience with C#.

So first, here is the refactored software under test, with methods increment() and decrement() as module functions. The interface does change, but the functionality is the same:

# 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

Add functions increment_mock() and decrement_mock() to 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

And now for the good part. Patch increment() and decrement() to replace them with their mocks:

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