How to mock Python static methods and class methods
mocking, python, unit-testing
Solution
C#, Java and C++ programmers tend to overuse class and static methods in Python. The Pythonic approach is to use module functions.
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)
Problem
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()`?