How do I mock a class that has unbound methods? For example, this class has a @classmethod
and a @staticmethod
:
class Calculator(objec
You were patching the wrong object. You must patch the Calculator
from the Machine
class, not the general Calculator
class. Read about it 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
One way to do it is
def test_increment(mocker):
mocker.patch.object(Calculator, attribute='increment', return_value=10)
...actual test code...
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)