# Как использовать pytest-mock для симуляции реальных ответов?

Аноним Автор

Одна из самых больших проблем, с которой сталкивается инженер-электрик, компьютерщик или инженер-конструктор - это преодоление разрыва между аппаратным и программным обеспечением. Задача написания программного обеспечения для работы с аппаратными средствами всегда оказывалась сложной. Это связано в первую очередь с тем, что основное внимание уделяется изучению синтаксиса языка и недостаточно времени уделяется отладке и тестированию программного обеспечения. Разрыв увеличивается еще больше, когда требуется доказать, что программное обеспечение не создает проблем при работе с аппаратным обеспечением. Это, как правило, приводит к многочасовому тестированию всей системы для обеспечения повторяемости, что не является масштабируемым решением при использовании аппаратного обеспечения из-за медленной реакции. Для разработчиков на Python решением является написание модульных тестов тестового кода с использованием pytest и плагина pytest-mock для имитации аппаратных ответов.


ПРИМЕЧАНИЕ: Хотя этот пост написан для разработчика аппаратного обеспечения, эта концепция также распространяется на любой внешний процесс или систему, например, базу данных.


## Код драйвера


from time import sleep from fabric import Connection class Driver: ═ ═ def __init__(self, hostname): ═ ═ ═ ═ self.connection = Connection(hostname) def run(self, command): ═ ═ ═ ═ sleep(5) ═ ═ ═ ═ return self.connection.run(command, hide=True).stdout.strip() def disk_free(self): ═ ═ ═ ═ return self.run("df -h"). @staticmethod ═ ═ def extract_percent(output): ═ ═ ═ ═ free_line = output.split("\n")[1] ═ ═ ═ ═ percent = free_line.split()[4] return percent


Приведенный выше драйвер является простым примером. Он использует библиотеку Fabric для установления SSH-соединения. Есть два метода:


• run() - позволяет выполнить любую общую команду на целевом компьютере и возвращает необработанный вывод. Перед возвратом выходных данных вставляется пятисекундная задержка для имитации медленного ответа.


• disk_free() - генерирует команду "df -h", а затем вызывает метод run() со сгенерированной командой.


• extract_percent() - форматирует вывод и возвращает процент свободного пространства диска.


## Тестовый код в интегрированном стиле


import socket from mock_tutorial.driver import Driver def test_driver_integrated(): ═ ═ d = Driver(socket.gethostname()) ═ ═ result = d.disk_free() ═ ═ percent = d.extract_percent(result) ═ ═ assert percent == "75%"


Приведенный выше тестовый код обычно пишется в стиле тестирования "черного ящика" для проверки этого драйвера. Сначала инстанцируется объект драйвера, вызывается функция disk_free(), разбирается вывод, а затем сравнивается с ожидаемым результатом.


$ pytest -s ======================== начинается сеанс тестирования ======================= платформа linux -- Python 3.9.4, pytest-6.2.3, py-1.10.0, pluggy-0.13.1 rootdir: /home/chinghwa/projects/mock_tutorial плагины: mock-3.5.1 собран 1 элемент tests/test_driver.py . ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═[100%] ========================= 1 пройден за 5.51с ========================


В дополнение к медленному выполнению в 5.51 секунды, есть еще одна проблема. Вывод "df -h", скорее всего, будет меняться со временем, а не оставаться на уровне 75%. Хотя этот пример надуманный, он иллюстрирует необходимость проверки вывода другим способом.


## Тестовый код с pytest-mock для имитации ответа


import socket import pytest from mock_tutorial.driver import Driver def test_driver_unit(mocker): ═ ═ output_list = [ ═ ═ ═ ═ "Filesystem Size Used Avail Use% Mounted on", ═ ═ ═ ═ "rootfs 472G 128G 344G 28% /", ═ ═ ═ ═ "none 472G 128G 344G 28% /dev", ] ═ ═ output = "\n".join(output_list) ═ ═ mock_run = mocker.patch( "mock_tutorial.driver.Driver.run", return_value=output ) ═ ═ d = Driver(socket.gethostname()) ═ ═ result = d.disk_free() ═ ═ percent = d.extract_percent(result) ═ ═ mock_run.assert_called_with("df -h") assert percent == "28%"


Приведенный выше код был переписан для использования объекта mock для мокирования метода run(). Сначала нужно импортировать pytest (строка 2) и вызвать фикстуру mocker из pytest-mock (строка 5).


В строках 12-14 метод run() класса Driver был исправлен заранее заданным ответом, чтобы имитировать реальный ответ. Это означает, что любой вызов run() будет возвращать строковое значение вывода.


В строке 18 будет проверена команда, которая была отправлена методу run(). При вызове метода disk_free() будет сгенерирована команда "df -h" и вызов run() с этой командой.


В строке 19 будет проверена функция синтаксического анализа, которая извлекает проценты из вывода. Если значение Use% в строке 8 будет изменено, то произойдет сбой, так как именно это значение будет извлечено.


$ pytest -s ======================== начинается сеанс тестирования ======================= платформа linux -- Python 3.9.4, pytest-6.2.3, py-1.10.0, pluggy-0.13.1 rootdir: /home/chinghwa/projects/mock_tutorial плагины: mock-3.5.1 собран 1 элемент tests/test_driver.py . ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═[100%] ========================= 1 пройден за 0.36с ========================


После добавления mock-объектов время теста сократилось до 0,36 секунды. Метод slow run() был пропатчен для более быстрого выполнения, а также был проверен код для разбора симулированного вывода.


## Резюме


Это должно послужить хорошей отправной точкой для разработки быстродействующих модульных тестов на Python путем исправления медленных ответов с помощью объектов-моков. В дополнение к модульным тестам следует также писать интеграционные тесты, однако они могут выполняться реже.


Этот пост - первый, который я написал на эту тему в Python, и я надеюсь в будущем углубиться в другие методы pytest-mock.


## Ресурсы


• Код в этой статье - https://github.com/chinghwayu/mock_tutorial


• pytest fixtures - https://docs.pytest.org/en/stable/fixture.html


• методы pytest-mock - https://github.com/pytest-dev/pytest-mock/


• pytest markers и маркировка тестов как медленных - https://docs.pytest.org/en/stable/example/markers.html