Почему тестирование стало частью работы разработчика
Когда речь заходит о тестировании, многие до сих пор представляют себе исключительно работу QA-инженеров. На практике современная разработка давно ушла от такого разделения ролей. Сегодня автоматизированные тесты пишут и сами разработчики, причем зачастую именно они создают большую часть тестового покрытия проекта.
Причина довольно проста. Любое приложение постоянно развивается: появляются новые функции, исправляются ошибки, обновляются зависимости, проводится рефакторинг. Каждое изменение потенциально может затронуть уже работающий код, и чем крупнее проект, тем выше вероятность случайно нарушить существующую функциональность.
Конечно можно каждый раз проверять приложение вручную. Но когда проект состоит из десятков или сотен модулей, такой подход быстро становится слишком дорогим по времени. Автоматизированные тесты позволяют буквально за несколько секунд убедиться, что изменения не привели к неожиданным последствиям.
Экосистема Python отлично подходит для такого подхода. Помимо стандартного модуля unittest, большинство современных проектов используют pytest, который предлагает лаконичный синтаксис, удобные фикстуры и множество готовых инструментов для тестирования. Но независимо от выбранной библиотеки принципы остаются одинаковыми: важно понимать, какие виды тестирования существуют и какую задачу решает каждый из них.
Причина довольно проста. Любое приложение постоянно развивается: появляются новые функции, исправляются ошибки, обновляются зависимости, проводится рефакторинг. Каждое изменение потенциально может затронуть уже работающий код, и чем крупнее проект, тем выше вероятность случайно нарушить существующую функциональность.
Конечно можно каждый раз проверять приложение вручную. Но когда проект состоит из десятков или сотен модулей, такой подход быстро становится слишком дорогим по времени. Автоматизированные тесты позволяют буквально за несколько секунд убедиться, что изменения не привели к неожиданным последствиям.
Экосистема Python отлично подходит для такого подхода. Помимо стандартного модуля unittest, большинство современных проектов используют pytest, который предлагает лаконичный синтаксис, удобные фикстуры и множество готовых инструментов для тестирования. Но независимо от выбранной библиотеки принципы остаются одинаковыми: важно понимать, какие виды тестирования существуют и какую задачу решает каждый из них.
Юнит-тестирование: проверяем отдельные части программы
Юнит-тестирование (Unit Testing) — это проверка самой маленькой независимой единицы программы: функции, метода или класса. Главная идея заключается в том, что тест должен проверять только одну конкретную часть логики, не затрагивая внешние зависимости.
Например, если функция рассчитывает стоимость заказа, ей не нужно во время тестирования обращаться к базе данных или выполнять HTTP-запросы. Все, что интересует разработчика, — правильно ли работает сама логика вычислений.
Представим простую функцию:
Например, если функция рассчитывает стоимость заказа, ей не нужно во время тестирования обращаться к базе данных или выполнять HTTP-запросы. Все, что интересует разработчика, — правильно ли работает сама логика вычислений.
Представим простую функцию:
Проверить ее можно буквально несколькими строками:
На первый взгляд подобный тест выглядит слишком простым. Однако именно такие небольшие проверки становятся основой надежного проекта. Через несколько месяцев после написания функции ее логика может измениться, появятся новые условия или дополнительные вычисления. Если случайно будет допущена ошибка, тест обнаружит ее сразу после запуска.
Поэтому в крупных проектах количество юнит-тестов нередко исчисляется тысячами. Каждый из них занимает доли секунды, но вместе они позволяют разработчикам уверенно проводить рефакторинг, не опасаясь сломать уже существующий функционал.
При этом хороший unit-тест обладает несколькими важными свойствами. Он должен быть быстрым, независимым от других тестов, всегда выдавать одинаковый результат и проверять только один сценарий. Если тест обращается к сети, зависит от содержимого базы данных или выполняется слишком долго, скорее всего, он уже перестает быть полноценным юнит-тестом.
Поэтому в крупных проектах количество юнит-тестов нередко исчисляется тысячами. Каждый из них занимает доли секунды, но вместе они позволяют разработчикам уверенно проводить рефакторинг, не опасаясь сломать уже существующий функционал.
При этом хороший unit-тест обладает несколькими важными свойствами. Он должен быть быстрым, независимым от других тестов, всегда выдавать одинаковый результат и проверять только один сценарий. Если тест обращается к сети, зависит от содержимого базы данных или выполняется слишком долго, скорее всего, он уже перестает быть полноценным юнит-тестом.
Еще одна распространенная ошибка — проверять только “идеальный” сценарий работы функции. Намного полезнее предусмотреть граничные случаи и некорректные данные.
Например, если функция должна запрещать отрицательные значения скидки, это условие лучше сразу заложить в ее реализацию:
Например, если функция должна запрещать отрицательные значения скидки, это условие лучше сразу заложить в ее реализацию:
Теперь можно протестировать не только корректные сценарии, но и поведение функции при неверных входных данных:
Подобные проверки особенно ценны. Они гарантируют, что приложение корректно реагирует не только на правильные данные, но и на ошибки пользователя или других компонентов системы.
Интеграционное тестирование: проверяем взаимодействие компонентов
Даже если каждая функция в приложении работает идеально, это еще не означает, что вся система будет работать без ошибок. В большинстве современных приложений отдельные модули постоянно взаимодействуют между собой: сервисы получают данные из базы, обращаются к внешним API, отправляют сообщения в очереди или сохраняют файлы.
Именно это взаимодействие и проверяют интеграционные тесты.
Каждый из этих компонентов может быть отдельно протестирован с помощью unit-тестов. Однако только интеграционный тест способен показать, что вся цепочка действительно работает корректно.
Именно поэтому такие тесты используют реальные зависимости: настоящую базу данных, файловую систему или тестовую версию внешнего API. Они выполняются значительно медленнее, зато позволяют обнаружить ошибки, которые невозможно найти при изолированном тестировании.
Например, отдельная функция может корректно создавать объект заказа, а проблема проявится только в момент записи данных в базу из-за несовпадения типов полей. Или приложение успешно получает ответ от внешнего сервиса, но не может корректно обработать изменившийся формат данных. Подобные ситуации невозможно выявить обычными unit-тестами.
Из-за использования реальных зависимостей интеграционных тестов обычно значительно меньше. Они дороже в поддержке и требуют более сложной инфраструктуры, поэтому их применяют для проверки наиболее важных пользовательских сценариев.
Именно это взаимодействие и проверяют интеграционные тесты.
- Представим интернет-магазин.
Каждый из этих компонентов может быть отдельно протестирован с помощью unit-тестов. Однако только интеграционный тест способен показать, что вся цепочка действительно работает корректно.
Именно поэтому такие тесты используют реальные зависимости: настоящую базу данных, файловую систему или тестовую версию внешнего API. Они выполняются значительно медленнее, зато позволяют обнаружить ошибки, которые невозможно найти при изолированном тестировании.
Например, отдельная функция может корректно создавать объект заказа, а проблема проявится только в момент записи данных в базу из-за несовпадения типов полей. Или приложение успешно получает ответ от внешнего сервиса, но не может корректно обработать изменившийся формат данных. Подобные ситуации невозможно выявить обычными unit-тестами.
Из-за использования реальных зависимостей интеграционных тестов обычно значительно меньше. Они дороже в поддержке и требуют более сложной инфраструктуры, поэтому их применяют для проверки наиболее важных пользовательских сценариев.
TDD: сначала тест, потом код
Большинство разработчиков сначала пишут код, а уже затем проверяют, работает ли он правильно. Методология Test Driven Development, или TDD, предлагает совершенно другой подход.
Вместо реализации функции разработчик сначала создает тест, который описывает ожидаемое поведение будущего кода. Разумеется, первый запуск заканчивается ошибкой, ведь сама функция еще не существует. После этого пишется минимальная реализация, позволяющая пройти тест, а затем выполняется рефакторинг.
На первый взгляд этот процесс кажется более медленным.
Зачем сначала писать тест для кода, которого еще нет?
На практике TDD заставляет разработчика думать не о реализации, а о поведении программы. Сначала определяется, какой результат должна выдавать функция, какие параметры она принимает и как должна реагировать на различные входные данные. Только после этого начинается написание самой логики.
Такой подход помогает создавать более простые и независимые компоненты. Если функцию сложно протестировать, это нередко говорит о том, что она берет на себя слишком много обязанностей и ее стоит разделить на несколько частей.
При этом TDD нельзя назвать универсальным решением. Если разработчик исследует новую библиотеку, создает прототип или экспериментирует с архитектурой, сначала написать рабочий код зачастую оказывается проще. Но при разработке бизнес-логики, которая будет долго поддерживаться и развиваться, TDD помогает значительно сократить количество ошибок и сделать код более предсказуемым.
Вместо реализации функции разработчик сначала создает тест, который описывает ожидаемое поведение будущего кода. Разумеется, первый запуск заканчивается ошибкой, ведь сама функция еще не существует. После этого пишется минимальная реализация, позволяющая пройти тест, а затем выполняется рефакторинг.
- Такой цикл принято описывать тремя словами: Red → Green → Refactor.
На первый взгляд этот процесс кажется более медленным.
Зачем сначала писать тест для кода, которого еще нет?
На практике TDD заставляет разработчика думать не о реализации, а о поведении программы. Сначала определяется, какой результат должна выдавать функция, какие параметры она принимает и как должна реагировать на различные входные данные. Только после этого начинается написание самой логики.
Такой подход помогает создавать более простые и независимые компоненты. Если функцию сложно протестировать, это нередко говорит о том, что она берет на себя слишком много обязанностей и ее стоит разделить на несколько частей.
При этом TDD нельзя назвать универсальным решением. Если разработчик исследует новую библиотеку, создает прототип или экспериментирует с архитектурой, сначала написать рабочий код зачастую оказывается проще. Но при разработке бизнес-логики, которая будет долго поддерживаться и развиваться, TDD помогает значительно сократить количество ошибок и сделать код более предсказуемым.
Mocking: как тестировать код без реальных зависимостей
Практически любое современное приложение взаимодействует с внешним миром. Оно отправляет HTTP-запросы, обращается к базе данных, получает данные из очередей сообщений, работает с файловой системой или использует сторонние API. Если каждый тест будет выполнять реальные запросы, он быстро станет медленным и нестабильным.
Представьте, что во время тестирования сервис должен получить курс валют из внешнего API. Даже если ваш код написан идеально, тест может завершиться с ошибкой просто потому, что сервер временно недоступен или интернет-соединение оказалось нестабильным. В результате проблема будет не в приложении, а во внешней зависимости.
! Именно для таких случаев используется mocking — подмена реальных объектов их имитациями.
Вместо настоящего HTTP-запроса тест получает заранее подготовленный ответ. Вместо обращения к базе данных используется объект, который ведет себя так же, как настоящая база, но существует только в памяти. Благодаря этому тест становится полностью независимым от внешнего окружения и проверяет только ту логику, которая действительно интересует разработчика.
В Python для этого чаще всего используют модуль unittest.mock:
Представьте, что во время тестирования сервис должен получить курс валют из внешнего API. Даже если ваш код написан идеально, тест может завершиться с ошибкой просто потому, что сервер временно недоступен или интернет-соединение оказалось нестабильным. В результате проблема будет не в приложении, а во внешней зависимости.
! Именно для таких случаев используется mocking — подмена реальных объектов их имитациями.
Вместо настоящего HTTP-запроса тест получает заранее подготовленный ответ. Вместо обращения к базе данных используется объект, который ведет себя так же, как настоящая база, но существует только в памяти. Благодаря этому тест становится полностью независимым от внешнего окружения и проверяет только ту логику, которая действительно интересует разработчика.
В Python для этого чаще всего используют модуль unittest.mock:
В этом примере метод get_rate() никогда не обращается к настоящему сервису. Вместо этого он сразу возвращает заранее определенное значение. Такой подход позволяет проверить, как приложение обрабатывает полученные данные, не беспокоясь о работе внешнего API.
Mock-объекты могут не только возвращать значения, но и проверять, был ли вызван определенный метод, сколько раз он вызывался и с какими аргументами. Это особенно полезно, если важно убедиться, что приложение действительно отправило письмо пользователю, вызвало нужный сервис или сохранило данные в репозиторий.
При этом использовать mocking стоит с осторожностью. Если в одном тесте приходится подменять практически все зависимости, это может говорить о том, что компоненты приложения слишком тесно связаны между собой. В такой ситуации лучше задуматься не о количестве mock-объектов, а о том, как упростить архитектуру самого приложения.
Mock-объекты могут не только возвращать значения, но и проверять, был ли вызван определенный метод, сколько раз он вызывался и с какими аргументами. Это особенно полезно, если важно убедиться, что приложение действительно отправило письмо пользователю, вызвало нужный сервис или сохранило данные в репозиторий.
При этом использовать mocking стоит с осторожностью. Если в одном тесте приходится подменять практически все зависимости, это может говорить о том, что компоненты приложения слишком тесно связаны между собой. В такой ситуации лучше задуматься не о количестве mock-объектов, а о том, как упростить архитектуру самого приложения.
Monkey Patching: меняем поведение объектов во время выполнения
Рядом с mocking часто упоминают еще одну технику — monkey patching. Несмотря на схожее назначение, это не одно и то же.
Если mocking обычно создает отдельные тестовые объекты, то monkey patching изменяет уже существующие функции или объекты прямо во время выполнения программы. Благодаря динамической природе Python сделать это можно буквально одной строкой.
Предположим, что в коде используется функция time.sleep(). Во время тестирования ждать несколько секунд после каждого вызова совсем не хочется:
Если mocking обычно создает отдельные тестовые объекты, то monkey patching изменяет уже существующие функции или объекты прямо во время выполнения программы. Благодаря динамической природе Python сделать это можно буквально одной строкой.
Предположим, что в коде используется функция time.sleep(). Во время тестирования ждать несколько секунд после каждого вызова совсем не хочется:
После такой подмены вызов time.sleep() перестанет делать паузу, и тесты будут выполняться значительно быстрее.
На практике разработчики редко изменяют объекты вручную подобным образом. Гораздо удобнее использовать встроенную фикстуру monkeypatch, которую предоставляет pytest. Она позволяет временно заменить функцию, переменную окружения или атрибут объекта, а после завершения теста автоматически вернуть исходное состояние.
Например, вместо настоящего адреса API можно подставить тестовый URL или временно заменить функцию, возвращающую текущую дату. Это особенно удобно при тестировании кода, который зависит от времени, случайных чисел или настроек окружения.
Несмотря на удобство, monkey patching не стоит использовать без необходимости. Подобные изменения могут сделать тесты менее очевидными для других разработчиков. Если, читая тест, приходится долго выяснять, какие функции были переопределены и почему, значит подмена используется слишком активно.
На практике разработчики редко изменяют объекты вручную подобным образом. Гораздо удобнее использовать встроенную фикстуру monkeypatch, которую предоставляет pytest. Она позволяет временно заменить функцию, переменную окружения или атрибут объекта, а после завершения теста автоматически вернуть исходное состояние.
Например, вместо настоящего адреса API можно подставить тестовый URL или временно заменить функцию, возвращающую текущую дату. Это особенно удобно при тестировании кода, который зависит от времени, случайных чисел или настроек окружения.
Несмотря на удобство, monkey patching не стоит использовать без необходимости. Подобные изменения могут сделать тесты менее очевидными для других разработчиков. Если, читая тест, приходится долго выяснять, какие функции были переопределены и почему, значит подмена используется слишком активно.
Что выбрать на практике
Между рассмотренными подходами нет конкуренции. Каждый из них решает свою задачу.
- Юнит-тесты помогают быстро проверить отдельные функции и классы. Они выполняются за доли секунды и позволяют без опасений проводить рефакторинг.
- Интеграционные тесты проверяют, что разные компоненты приложения действительно умеют работать вместе. Именно они позволяют обнаружить проблемы взаимодействия сервисов, баз данных и внешних API.
- Mocking делает тесты независимыми от внешнего окружения и позволяет сосредоточиться на проверке бизнес-логики, а monkey patching дает дополнительную гибкость в ситуациях, когда необходимо временно изменить поведение существующих объектов.
- Что касается TDD, то это скорее подход к разработке, чем разновидность тестирования. Он помогает заранее продумывать поведение программы и часто приводит к появлению более простого и хорошо тестируемого кода.
Автоматизированное тестирование давно перестало быть дополнительной задачей, которой занимаются только QA-инженеры. Сегодня это один из основных инструментов разработчика, позволяющий быстрее выпускать новые версии приложений и не бояться изменений в существующем коде.
При этом не существует единственного “правильного” способа тестирования.
Главное — не стремиться написать как можно больше тестов ради красивых цифр покрытия. Гораздо важнее, чтобы тесты действительно проверяли критичные сценарии, быстро выполнялись и помогали разработчикам уверенно вносить изменения в проект. Именно такие тесты становятся не формальностью, а полноценным инструментом поддержки качества кода.
Хотите узнать больше? Изучите другие статьи из раздела:
При этом не существует единственного “правильного” способа тестирования.
Главное — не стремиться написать как можно больше тестов ради красивых цифр покрытия. Гораздо важнее, чтобы тесты действительно проверяли критичные сценарии, быстро выполнялись и помогали разработчикам уверенно вносить изменения в проект. Именно такие тесты становятся не формальностью, а полноценным инструментом поддержки качества кода.
Хотите узнать больше? Изучите другие статьи из раздела: