I am not sure about the title of this question, as it is not easy to describe the issue with a single sentence. If anyone can suggest a better title, I'll edit it.
Consider this code that uses smbus2
to communicate with an I2C device:
# device.py
import smbus2
def set_config(bus):
write = smbus2.i2c_msg.write(0x76, [0x00, 0x01])
read = smbus2.i2c_msg.read(0x76, 3)
bus.i2c_rdwr(write, read)
I wish to unit-test this without accessing I2C hardware, by mocking the smbus2
module as best I can (I've tried mocking out the entire smbus2
module, so that it doesn't even need to be installed, but had no success, so I'm resigned to importing smbus2
in the test environment even if it's not actually used - no big deal so far, I'll deal with that later):
# test_device.py
# Depends on pytest-mock
import device
def test_set_config(mocker):
mocker.patch('device.smbus2')
smbus = mocker.MagicMock()
device.set_config(smbus)
# assert things here...
breakpoint()
At the breakpoint, I'm inspecting the bus
mock in pdb:
(Pdb) p smbus
<MagicMock id='140160756798784'>
(Pdb) p smbus.method_calls
[call.i2c_rdwr(<MagicMock name='smbus2.i2c_msg.write()' id='140160757018400'>, <MagicMock name='smbus2.i2c_msg.read()' id='140160757050688'>)]
(Pdb) p smbus.method_calls[0].args
(<MagicMock name='smbus2.i2c_msg.write()' id='140160757018400'>, <MagicMock name='smbus2.i2c_msg.read()' id='140160757050688'>)
(Pdb) p smbus.method_calls[0].args[0]
<MagicMock name='smbus2.i2c_msg.write()' id='140160757018400'>
Unfortunately, at this point, the arguments that were passed to write()
and read()
have been lost. They do not seem to have been recorded in the smbus
mock and I've been unable to locate them in the data structure.
Interestingly, if I break in the set_config()
function, just after write
and read
assignment, and inspect the mocked module, I can see:
(Pdb) p smbus2.method_calls
[call.i2c_msg.write(118, [160, 0]), call.i2c_msg.read(118, 3)]
(Pdb) p smbus2.method_calls[0].args
(118, [160, 0])
So the arguments have been stored as a method_call in the smbus2
mock, but not copied over to the smbus
mock that is passed into the function.
Why is this information not retained? Is there a better way to test this function?
I think this can be summarised as this:
In [1]: from unittest.mock import MagicMock
In [2]: foo = MagicMock()
In [3]: bar = MagicMock()
In [4]: w = foo.write(1, 2)
In [5]: r = foo.read(1, 2)
In [6]: bar.func(w, r)
Out[6]: <MagicMock name='mock.func()' id='140383162348976'>
In [7]: bar.method_calls
Out[7]: [call.func(<MagicMock name='mock.write()' id='140383164249232'>, <MagicMock name='mock.read()' id='140383164248848'>)]
Note that the bar.method_calls
list contains calls to the functions .write
and .read
(good), but the parameters that were passed to those functions are missing (bad). This seems to undermine the usefulness of such mocks, since they don't interact as I would expect. Is there a better way to handle this?
For anyone coming across this, I posed a variation of this problem in another question, and the result was quite satisfactory:
https://stackoverflow.com/a/73739343/
In a nutshell, create a TraceableMock
class, derived from MagicMock
, that returns a new mock that keeps track of its parent, as well as the parameters of the function call that led to this mock being created. Together, there is enough information to verify that the correct function was called, and the correct parameters were supplied.