Python Mocking — как получить аргументы вызова из макета, который передается другому макету в качестве аргумента функции?

Я не уверен в названии этого вопроса, так как непросто описать проблему одним предложением. Если кто-то может предложить лучшее название, я его отредактирую.

Рассмотрим этот код, который использует 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 (хорошо), но параметры, которые были переданы этим функциям, отсутствуют (плохо). Кажется, это подрывает полезность таких макетов, поскольку они не взаимодействуют так, как я ожидал. Есть ли лучший способ справиться с этим?

Код для set_config принимает параметр bus, но, похоже, не использует его — скорее, он напрямую использует модуль smbus. Может ли это быть проблемой?

Samuel Dion-Girardeau 16.09.2022 07:19

Вы пробовали простоmocker.patch('smbus2')?

Adrian 16.09.2022 22:41

@SamuelDion-Girardeau ой - почему-то в коде, который я вставил, отсутствует строка - извините! Я исправлю это сейчас. В недостающей строке действительно используется bus.

davidA 19.09.2022 00:00

@ Адриан, да, я пробовал это, и это приводит к мокам для переменных read и write, что нормально, но они не содержат параметры, используемые для их создания, потому что вместо этого они хранятся в макете smbus2.i2c_msg. Поэтому они не связаны с макетом bus, и тест не может установить надежное соединение (макет smbus2 может быть проверен тестом, и параметры там присутствуют, но когда больше вызовов i2c_msg.read/write made, ссылки на этот конкретный звонок нет, так что это бесполезно).

davidA 19.09.2022 00:04

@davidA Спасибо за обновление, теперь это имеет больше смысла! Просто бросил ответ, надеюсь, что это поможет.

Samuel Dion-Girardeau 19.09.2022 21:54
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
5
126
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Я только что понял, что вы используете внедрение зависимостей и что вы должны воспользоваться этим.

  1. Это будет чистый подход.
  2. Насмешки могут вести себя неожиданно/противно (что не означает, что они злые - только иногда... противоречащие здравому смыслу)

Я бы рекомендовал следующую структуру теста:

# 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 были вызваны с правильными параметрами.

davidA 20.09.2022 01:38

Причина, по которой вы не можете получить доступ к вызовам 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 я смогу их достаточно изолировать. Я буду упорствовать...

davidA 20.09.2022 01:41
Ответ принят как подходящий

Для всех, кто сталкивался с этим, я задал вариант этой проблемы в другом вопросе, и результат был вполне удовлетворительным:

https://stackoverflow.com/a/73739343/

Короче говоря, создайте класс TraceableMock, производный от MagicMock, который возвращает новый макет, который отслеживает своего родителя, а также параметры вызова функции, который привел к созданию этого макета. В совокупности имеется достаточно информации, чтобы убедиться, что была вызвана правильная функция и были предоставлены правильные параметры.

Другие вопросы по теме

С#: как получить доступ к переменным из обработчика событий с помощью контейнера DI, такого как Simple Injector?
Как я могу использовать внедрение зависимостей в объект, созданный с помощью Activator with Factory Pattern?
Как передать метод службы обработчика событий после регистрации DbContext? Если служба обработчика событий зависит от службы DbContext
.net core прерывает инъекцию зависимостей с помощью универсального интерфейса
Ошибка C# MediatR: зарегистрируйте свои обработчики в контейнере
Как выбрать правильную стратегию во время выполнения при реализации шаблона стратегии?
APP_INITIALIZER не ожидает (Angular 13)
Вызов конструктора не по умолчанию для концепции
Невозможно заставить Polly повторить вызовы Http при возникновении заданных исключений
Не удается выполнить разрешение от корневого поставщика, поскольку для этого требуется служба с ограниченной областью действия