Несколько недель назад я наткнулся на эту замечательную лекцию Брэндона Родса. Один из ключевых выводов, который я сделал - это важность отделения операций ввода-вывода (т.е. сетевых запросов, обращений к базе данных и т.д.) от основной логики нашего кода. Это позволяет сделать наш код более модульным и тестируемым. В этой статье я не буду углубляться в тонкости "чистой архитектуры" и перейти сразу к практике! Если вы нашли ошибку - используйте, пожалуйста, Ctrl+Enter. Представьте, что у нас есть функция find_definition, которая выполняет обработку данных и включает в себя HTTP-запросы к внешнему API. Для написания модульного теста для функции find_definition мы используем модуль unittest. Пример: Чтобы изолировать операции ввода-вывода, мы используем декоратор patch из модуля unittest.mock. Он позволяет нам имитировать поведение функции requests.get. Таким образом, мы можем контролировать ответ, который получает наша функция во время тестирования и мы можем изолированно тестировать функцию find_definition, не выполняя реальных HTTP-запросов. Используя декоратор patch для имитации поведения функции requests.get, мы жестко привязываем тесты к внутренней работе функции. Это делает тесты более восприимчивыми к поломкам при изменениях в реализации или зависимостях. Если реализация find_definition изменится, например в случаях: использование другой библиотеки HTTP изменение структуры ответа API изменение конечной точки API Возможно, потребуется соответствующее обновление тестов. В случае find_definition написание и сопровождение модульных тестов превращается в громоздкую задачу! Обычно при работе с функциями типа find_definition, включающими операции ввода-вывода, я часто провожу рефакторинг кода, чтобы вынести операции ввода-вывода в отдельную функцию, например call_json_api, как показано в обновленном коде ниже: Вынося операции ввода-вывода в отдельную функцию, мы достигаем определенного уровня абстракции и инкапсуляции. Теперь функция find_definition делегирует функции call_json_api ответственность за выполнение HTTP-запроса и разбор JSON-ответа. И снова мы используем декоратор patch из модуля unittest.mock, чтобы сымитировать поведение функции call_json_api (вместо requests.get). Таким образом, мы можем контролировать ответ, который получает find_definition во время тестирования. Однако важно отметить, что, хотя мы и спрятали операции ввода-вывода за функцией call_json_api, мы не полностью их разделили. Функция find_definition по-прежнему зависит от деталей реализации call_json_api и предполагает, что она будет корректно обрабатывать операции ввода-вывода. Для достижения цели разделения этих функции ввода-вывода мы можем использовать внедрение зависимостей. Обновленная версия функции find_definition: Добавлен параметр api_client, который представляет зависимость, отвечающую за выполнение вызовов API. По умолчанию он имеет значение requests, что позволяет нам использовать библиотеку requests для операций ввода-вывода. Использование внедрения зависимостей позволяет улучшить контроль и предсказуемость при модульном тестировании. Вот пример того, как можно написать модульные тесты для функции find_definition с использованием инъекции зависимостей: В обновленном примере модульного теста мы создаем имитатор API-клиента, используя класс MagicMock из модуля unittest.mock. Mock API-клиент настроен на возврат предопределенного ответа, т.е. mock_response, при вызове метода get. Ура! Теперь, в случае, если мы захотим использовать другую библиотеку HTTP, мы окажемся в гораздо более выгодном положении. Хотя внедрение зависимостей дает ряд преимуществ, оно также может создавать некоторые проблемы. Как отметил Брэндон, существует несколько потенциальных проблем, о которых следует знать: Макет против реальной библиотеки: Макетные объекты, используемые для тестирования, могут не полностью повторять поведение реальных зависимостей. Это может привести к расхождениям между результатами тестирования и реальным поведением в процессе работы. Сложные зависимости: Функции или компоненты с множеством зависимостей, например, комбинация базы данных, файловой системы и внешних сервисов, могут потребовать значительной настройки и управления инъекциями, что усложняет кодовую базу. Это подводит нас к следующему вопросу. Стремясь к созданию гибкого и тестируемого кода, мы можем использовать другой подход, не прибегая к явному внедрению зависимостей. Мы можем добиться четкого разделения проблем, поместив операции ввода-вывода на внешний слой кода. Приведем пример, демонстрирующий эту концепцию: Здесь функция find_definition сосредоточена исключительно на основной логике извлечения определения из полученных данных. Операции ввода-вывода, такие, как выполнение HTTP-запроса и получение JSON-ответа, выполняются на внешнем уровне. Кроме того, функция find_definition также опирается на две отдельные функции:   функция build_url строит URL для API-запроса   Функция pluck_definition извлекает определение из ответа API. Добавим соответствующие фрагменты кода: Благодаря тому, что ввод-вывод вынесен на внешний слой, код становится более гибким. Мы успешно создали функции, которые можно тестировать по отдельности и заменять по мере необходимости. Например, можно легко переключиться на другую конечную точку API, модифицировав функцию build_url, или обработать альтернативные сценарии ошибок в функции pluck_definition. Для демонстрации гибкости и контроля, обеспечиваемых модульной конструкцией, давайте обновим наши модульные тесты для функции find_definition. В обновленных модульных тестах теперь есть отдельные методы тестирования для каждого из модульных компонентов: test_find_definition остался практически без изменений по сравнению с предыдущим примером до внедрения инъекции зависимостей, проверяя правильность поведения функции find_definition. Однако теперь она утверждает, что функция requests.get вызывается с URL, сгенерированным функцией build_url, демонстрируя обновленное взаимодействие между модульными компонентами. test_build_url проверяет, что функция build_url правильно строит URL на основе заданного слова. test_pluck_definition проверяет, что функция pluck_definition правильно извлекает определение из предоставленных данных. Обновив наши модульные тесты, мы теперь можем тестировать каждый компонент независимо, обеспечивая их корректную работу в отдельности. Мы кратко рассмотрели различные подходы к рефакторингу для решения проблемы тесной связи и достижения свободной связи между компонентами. Кроме того, мы увидели, как можно улучшить модульное тестирование за счет имитации операций ввода-вывода и управления поведением внешних зависимостей.  Ищу работу