Я не уверен в названии этого вопроса, так как непросто описать проблему одним предложением. Если кто-то может предложить лучшее название, я его отредактирую.
Рассмотрим этот код, который использует smbus2
для связи с устройством I2C:
# 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)
Я хочу провести модульное тестирование без доступа к оборудованию I2C, максимально смоделировав модуль smbus2
(я пытался смоделировать весь модуль smbus2
, чтобы его даже не нужно было устанавливать, но безуспешно, поэтому я смирился с импортом smbus2
в тестовую среду, даже если он на самом деле не используется — пока ничего страшного, я разберусь с этим позже):
# 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()
В точке останова я проверяю макет bus
в 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'>
К сожалению, на данный момент аргументы, которые были переданы в write()
и read()
, потеряны. Кажется, они не были записаны в макете smbus
, и я не смог найти их в структуре данных.
Интересно, что если я взломаю функцию set_config()
сразу после присваивания write
и read
и проверю фиктивный модуль, я увижу:
(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])
Таким образом, аргументы были сохранены как method_call в макете smbus2
, но не скопированы в макет smbus
, который передается в функцию.
Почему эта информация не сохраняется? Есть ли лучший способ проверить эту функцию?
Я думаю, что это можно резюмировать так:
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'>)]
Обратите внимание, что список bar.method_calls
содержит вызовы функций .write
и .read
(хорошо), но параметры, которые были переданы этим функциям, отсутствуют (плохо). Кажется, это подрывает полезность таких макетов, поскольку они не взаимодействуют так, как я ожидал. Есть ли лучший способ справиться с этим?
Вы пробовали простоmocker.patch('smbus2')
?
@SamuelDion-Girardeau ой - почему-то в коде, который я вставил, отсутствует строка - извините! Я исправлю это сейчас. В недостающей строке действительно используется bus
.
@ Адриан, да, я пробовал это, и это приводит к мокам для переменных read
и write
, что нормально, но они не содержат параметры, используемые для их создания, потому что вместо этого они хранятся в макете smbus2.i2c_msg
. Поэтому они не связаны с макетом bus
, и тест не может установить надежное соединение (макет smbus2
может быть проверен тестом, и параметры там присутствуют, но когда больше вызовов i2c_msg.read/write
made, ссылки на этот конкретный звонок нет, так что это бесполезно).
@davidA Спасибо за обновление, теперь это имеет больше смысла! Просто бросил ответ, надеюсь, что это поможет.
Я только что понял, что вы используете внедрение зависимостей и что вы должны воспользоваться этим.
Я бы рекомендовал следующую структуру теста:
# test_device.py
import device
def test_set_config():
dummy_bus = DummyBus()
device.set_config(dummy_bus)
# assert things here...
assert dummy_bus.read_data == 'foo'
assert dummy_bus.write_data == 'bar'
breakpoint()
class DummyBus:
def __init__(self):
self.read_data = None
self.write_data = None
def i2c_rdwr(write_input, read_input):
self.read_data = read_input
self.write_data = write_input
Спасибо за ответ. В целом мне нравится такой подход, и я согласен с тем, что Mocks не решает всего, однако есть нюанс, который не совсем охвачен вашим ответом - тестируемый код вызывает smbus2.i2c_msg.write
и smbus2.i2c_msg.read
для создания параметров, а в идеале этих тоже будут высмеиваться, поскольку они являются нетривиальными структурами данных. В моем текущем коде я позволяю set_config
вызывать функции, предоставленные smbus2
, а затем сравниваю с пользовательским утверждением, но это беспорядочно, и я бы предпочел найти способ проверить, что .read
и .write
были вызваны с правильными параметрами.
Причина, по которой вы не можете получить доступ к вызовам write
и read
, заключается в том, что они сами являются return_value
другого макета. То, что вы пытаетесь сделать, это получить доступ к макету «родителя» (используя терминологию здесь: https://docs.python.org/3/library/unittest.mock.html).
На самом деле можно получить доступ к родителю, но я не уверен, что это хорошая идея, поскольку он использует недокументированный и закрытый атрибут объекта MagicMock
, _mock_new_parent
.
def test_set_config(mocker):
"""Using the undocumented _mock_new_parent attribute"""
mocker.patch('device.smbus2')
smbus = mocker.MagicMock()
device.set_config(smbus)
# Retrieving the `write` and `read` values passed to `i2c_rdwr`.
mocked_write, mocked_read = smbus.i2c_rdwr.call_args[0]
# Making some assertions about how the corresponding functions were called.
mocked_write._mock_new_parent.assert_called_once_with(0x76, [0x00, 0x01])
mocked_read._mock_new_parent.assert_called_once_with(0x76, 3)
Вы можете проверить, что утверждения работают, используя вместо этого некоторые фиктивные значения, и вы увидите ошибки утверждения pytest.
Более простой и стандартный подход IMO заключается в непосредственном просмотре вызовов из макета модуля:
def test_set_config_2(mocker):
""" Using the module mock directly"""
mocked_module = mocker.patch('device.smbus2')
smbus = mocker.MagicMock()
device.set_config(smbus)
mocked_write = mocked_module.i2c_msg.write
mocked_read = mocked_module.i2c_msg.read
mocked_write.assert_called_once_with(0x76, [0x00, 0x01])
mocked_read.assert_called_once_with(0x76, 3)
Спасибо за ваш ответ. Я думаю, что более стандартный подход, вероятно, лучший способ сделать это. Сложность заключается в том, что есть другие вызовы .read
и .write
, которые мешают тем, которые я специально рассматриваю, но, возможно, при разумном использовании reset_mock
я смогу их достаточно изолировать. Я буду упорствовать...
Для всех, кто сталкивался с этим, я задал вариант этой проблемы в другом вопросе, и результат был вполне удовлетворительным:
https://stackoverflow.com/a/73739343/
Короче говоря, создайте класс TraceableMock
, производный от MagicMock
, который возвращает новый макет, который отслеживает своего родителя, а также параметры вызова функции, который привел к созданию этого макета. В совокупности имеется достаточно информации, чтобы убедиться, что была вызвана правильная функция и были предоставлены правильные параметры.
Код для
set_config
принимает параметрbus
, но, похоже, не использует его — скорее, он напрямую использует модульsmbus
. Может ли это быть проблемой?