Как НЕ нужно писать автотесты на Python
Разбираем самые странные антипаттерны в автотестах на Python: от sleep(0.1) и стрелочек вниз до глобальных курсоров и "фреймворков" на 3500 строк. Почему так делать не стоит и какие есть взрослые альтернативы.
Введение
В этой статье я разберу несколько типичных ошибок, которые встречаются при написании автотестов на Python. Цель не в том, чтобы высмеять конкретных людей или проекты. Главное — показать абсурдность некоторых подходов, объяснить, как не стоит строить тестовую инфраструктуру и почему это приводит к проблемам.
Задача простая: сэкономить вам время и силы. Чтобы не пришлось потом «переучиваться», избавляться от костылей и проходить болезненный детокс от самодельных «велосипедов». Гораздо продуктивнее с самого начала писать тесты так, чтобы код был качественным, понятным и поддерживаемым.
Дисклеймер. Примеры в статье обобщены и синтетически изменены; цель — разбирать решения, а не авторов. Любые совпадения с реальными проектами случайны. Все рекомендации — про архитектуру и практики, а не про людей.
История находки
Эта статья появилась не случайно. Недавно ко мне пришёл студент с курса и задал вопрос: «Я нашёл фреймворк для автотестов. Это вообще нормальная практика? Так делают?»
Когда я открыл ссылку и посмотрел код, увидел монолитный файл с перемешанными зонами ответственности. Передо мной оказалась «библиотека», которая позиционировала себя как универсальный фреймворк для автотестов «на все случаи жизни». Внутри — один-единственный файл на 3500 строк, в который было запихнуто всё подряд: UI-тесты, API-тесты, обёртки, тулзы, хелперы, нагрузочные тесты и даже системные утилиты. Получился не фреймворк, а монолит без архитектуры.
И самое удивительное: со слов студента, этот «фреймворк» преподносится как «лёгкий способ писать автотесты». В этой статье мы разберём, почему это совсем не лёгкий путь, а скорее быстрый путь к нестабильным тестам и техническому долгу.
Скажу сразу: я не буду давать ссылок и называть авторов. Цель статьи не в том, чтобы кого-то высмеивать или унизить. Цель — разобрать архитектурные ошибки, подсветить костыли, велосипеды и антипаттерны. Подобный код, увы, встречается не только здесь: он реально используется на проектах, да ещё и подаётся новичкам как «правильный подход».
Поэтому давайте вместе проведём небольшой «детокс» от подобных решений.
Антипаттерн 1. «Танцы со стрелочками вниз»
Симптом. В коде десятки функций вида «нажми стрелку вниз N раз, вдруг элемент окажется в видимой области». Часто ещё с time.sleep(0.1) в цикле и попыткой кликнуть «когда повезёт».
Плохой пример (сокращённо)
Что здесь не так?
- Flaky и гонки. time.sleep() маскирует проблему синхронизации, а не решает её. На CI такие тесты «мигают».
- Зависимость от фокуса. Клавиши работают только если нужный контейнер в фокусе. Любой поп-ап/модал — и всё сломалось.
- Дублирование/раздувание. Вариации «стрелка вниз/вверх/ENTER/SPACE» плодят десятки однотипных функций.
- Обход DOM-модели. Вместо явного скролла к элементу — «надеемся», что страница сама промотается.
- Смешение ожиданий. Параллельно могут быть неявные ожидания — итогом становятся непредсказуемые тайм-ауты.
Как правильно (коротко и надёжно)
Вариант по умолчанию — Playwright
Почему: автоожидания «из коробки», стабильные локаторы, нормальный скролл, перехват сети/консоли, меньше кода — меньше flaky.
- Никаких «стрелок вниз», sleep(0.1) и шаманства с ActionChains.
- Локаторы лучше писать не XPath-«простынями», а через data-test-id:page.get_by_test_id(locator).click().
Когда всё-таки Selenium?
Если проект уже на Selenium и переписать нельзя, сводим утилиты к минимуму и не используем клавиши как костыли:
Когда уместны клавиши?
Только если вы намеренно тестируете доступность/навигацию клавиатурой (Tab flow, меню-стрелки, хоткеи). Для «проскроллить и кликнуть» — это антипаттерн.
Мини-чеклист вместо «танцев»
- Playwright по умолчанию (автоожидания, стабильные локаторы).
- Если Selenium — только явные ожидания + scrollIntoView, без sleep.
- Один-два универсальных хелпера вместо десятков «стрелка вниз N раз».
- JS-клик — как исключение, а не как стратегия.
Антипаттерн 2. «exec в API» и прочая небезопасная магия
Симптом. Функция отправки HTTP-запроса выполняет произвольный код перед запросом, смешивает ответственность и не контролирует ошибки.
Плохой пример (сокращённо)
Что здесь не так?
- exec(pre_script) — выполнение произвольного кода из строки. Это уязвимость класса RCE. Доверие к данным ≠ повод их исполнять.
- Смешение ответственности. В одном методе «бизнес-логика препроцессинга», сериализация, сетевой вызов и «молчаливое» игнорирование ошибок.
- data=body вместо json=... — рискуете неверным Content-Type и кодировкой (и ручной сериализацией там, где она не нужна).
- Отсутствие таймаутов/ретраев — подвисания и flaky на CI.
- Нет возврата контракта. Неясно, что возвращает метод, как обрабатывать 4xx/5xx.
Как правильно?
Вариант 1. Небольшой «чистый» синхронный клиент на httpx
Вариант 2. Асинхронный клиент + ретраи (коротко)
(Опционально) Валидация данных через Pydantic
Мини-чеклист безопасности и здравого смысла
- Никаких exec, eval, «прескриптов» строкой.
- Сериализация — через json=; заголовки задаём явно, если нужно.
- Всегда таймауты; для нестабильных сетей — ретраи с экспонентой.
- Единый и предсказуемый контракт возврата (или исключения).
- Валидация входа/выхода (Pydantic) — меньше сюрпризов в тестах.
- Логи: метод, URL, статус, latency (без утечек чувствительных данных).
- Не используем bare except: ловите конкретные исключения httpx/requests
Антипаттерн 3. Глобальные connection/cursor
Симптом. Подключение к БД и курсор создаются один раз «где-то сверху», кладутся в глобальные переменные и дальше используются из любой функции.
Плохой пример (сокращённо)
Что здесь не так?
- Утечки и «висящие» транзакции. Глобальный коннект легко забыть закрыть; автокоммит / неявные транзакции висят между тестами.
- Не потокобезопасно. Параллельный запуск (pytest-xdist) или просто несколько тестов одновременно — и вы ловите гонки/«курсор уже закрыт».
- Неизолированные тесты. Один тест меняет состояние БД — другой видит мусор.
- Нельзя конфигурировать точечно. Хотите иной таймаут/роль/схему — увы, «у нас один на всех».
- Непрозрачные ошибки. Ошибка «где-то» в общем курсоре → падать начинает «всё» и отлаживать больно.
Как правильно?
Вариант A. Чистая функция + контекстные менеджеры (psycopg2)
- Каждый вызов сам управляет ресурсами — нет глобального состояния.
- Параметризация через args — защита от SQL-инъекций (никаких f"... {user_id} ...").
- Конфигурация (host, dbname, options/search_path) — на уровне вызова.
Вариант B. Пул соединений (если запросов много)
- Подойдёт для тестовых раннеров, которые часто ходят в БД.
- Всё ещё без глобального курсора и с аккуратным возвратом соединения.
Вариант C. SQLAlchemy (индустриальный стандарт)
- Менеджер соединений, параметризация, кросс-СУБД, удобные маппинги.
- Для сложных проектов — ORM/модели, миграции Alembic.
Тестовая изоляция (очень важно)
Чтобы тесты не пачкали БД и не зависели друг от друга — оборачиваем каждый тест в транзакцию и откатываем её.
Pytest-фикстуры на psycopg2
- Каждый тест получает чистое состояние, изменения не «протекают».
- Можно дополнить SAVEPOINT/begin_nested для более тонкой грануляции.
Мини-чеклист
- Никаких global connection, cursor.
- Всегда with connect() as conn, conn.cursor() as cur:.
- Параметризованные запросы (cur.execute(sql, args)), не f-строки с данными.
- Для массовых вызовов — пул соединений.
- Для реальных проектов — SQLAlchemy (Core/ORM) + миграции.
- В тестах — транзакция на тест и обязательный rollback.
- Разные СУБД — разные модули/клиенты, не «всё в одном классе».
- Разделяйте креды/DSN через переменные окружения (не хардкодим в коде).
Антипаттерн 4. «Тестовая библиотека сама ставит Node.js/Newman через sudo»
Симптом. Внутри «фреймворка автотестов» есть функция, которая лезет в ОС и устанавливает системные пакеты — Node.js, npm и Newman — причём разными путями для Windows/macOS/Linux, местами через sudo, местами скачивая MSI.
Плохой пример (сокращённо)
Что здесь не так?
- Нарушение границ ответственности. Тестовая библиотека не должна администрировать ОС. Это задача DevOps/окружения, а не кода в framework_for_tests.py.
- Безопасность. sudo, скачивание и установка бинарей «на лету» из тестов — это прямое приглашение к RCE/порче машины.
- Неповторяемость. Сегодня apt install nodejs поставил v18, завтра v22. Результаты «тестов» будут разными.
- Ломает CI/CD. Контейнеры собраны заранее. Любая попытка ставить системный софт во время теста — медленно, нестабильно и часто попросту запрещено.
- Скрытые побочки. Глобальная установка npm -g newman меняет окружение разработчика/агента. Откаты нет.
Как правильно?
Вариант A. Контейнер с зафиксированными зависимостями (рекомендуется)
Dockerfile (фрагмент):
- Всё ставится на этапе сборки, версии зафиксированы.
- В рантайме тесты не лезут в ОС.
Вариант B. Make/CI-оркестрация — не из тестовой либы
Makefile (фрагмент):
- Инсталляция и запуск — отдельные цели.
- Тестовая библиотека не знает про установку системных утилит.
Вариант C. Если Newman очень нужен — оборачивайте вызов с проверкой, но не устанавливайте
- Явно падаем с понятной ошибкой, если зависимости нет.
- Никаких sudo и «магии установки».
Альтернатива Newman: чистый Python или профильные инструменты
- Для API — httpx + pytest (и отчётность Allure).
- Для нагрузки — Locust/k6 (метрики, сценарии, профили).
- Для E2E — Playwright, у которого есть трейсинг/видео, сетевые HAR без плясок.
Мини-чеклист
- Никогда не ставим системный софт из тестовой библиотеки.
- Все системные зависимости — в Dockerfile или в CI шаге.
- Версии фиксируем (lockfile/теги).
- В тестовом коде — только проверка наличия инструмента и аккуратный вызов.
- По возможности заменяем «внешние CLI» на библиотечные вызовы в Python/Playwright/Locust.
Антипаттерн 5. «Нагрузка» через httpx и pytest.mark.asyncio
Симптом. Фреймворк называет «нагрузочным тестом» просто пачку параллельных HTTP-запросов через asyncio.gather, помеченных @pytest.mark.asyncio. Где-то печатается текст «Count = N», и на этом «перфоманс» заканчивается.
Плохой пример (сокращённо)
Что здесь не так?
- Это не нагрузочное тестирование. Нет профиля нагрузки (RPS/Concurrency/Duration), нет прогрева, нет стабилизации, нет измерений latency/percentiles, нет ошибок/таймаутов в отчёте, нет корреляции с метриками сервера (CPU, память, сеть).
- Смешение с pytest. Навешивание @pytest.mark.asyncio на утилиты ломает запуск вне pytest и «привязывает» код к раннеру тестов.
- Отсутствие контроля времени. asyncio.gather максимизирует параллелизм «сколько успели», но не удерживает профиль (RPS/конкарренси/длительность), поэтому данные о p95/p99 и SLA нерепрезентативны.
- Нереалистичный сценарий. Нет сессий, куков, заголовков, вариативности payload’ов, зависимостей между шагами.
- Никакой отчётности. Печать в консоль — это не репорт. Нужны агрегаты: p50/p90/p99, ошибки по кодам, пер-запросная статистика, графики.
Как правильно?
Использовать профильные инструменты (рекомендовано)
Locust (Python, сценарный подход):
Запуск с профилем нагрузки:
- -u 200: одновременно 200 пользователей,
- -r 20: разгон по 20 пользователей/сек,
- -t 10m: длительность 10 минут.
Locust отдаёт p50/p90/p95/p99, RPS, ошибки, можно экспортировать CSV и интегрировать с Grafana/Prometheus.
k6 (альтернатива): декларативные сценарии, отличный вывод метрик и удобная интеграция с Grafana/InfluxDB.
Что ещё важно для «настоящей нагрузки»
- Сеансы и данные. Реалистичные пользователи/куки/токены, вариативные payload’ы, подготовленные фикстуры/сидинг.
- Наблюдаемость. Корреляция RPS/latency с CPU/Memory/GC/DB/Cache. Без этого вы «стреляете в темноту».
- Профиль. Разгон, плато, спад; A/B сценарии; фоновые шумовые нагрузки.
- Отчёт. p50/p90/p99, Throughput, ошибки по классам (4xx/5xx/таймауты), пер-эндпойнт агрегации, SLA/SLO.
Мини-чеклист
- Не маскировать «пачку запросов» под «нагрузочное тестирование».
- Использовать Locust/k6 или хотя бы честный раннер с целевым профилем и метриками.
- Не вешать @pytest.mark.asyncio на утилиты — выносить раннер отдельно от тестов.
- Собирать метрики и строить отчёты; без этого выводы невалидны.
- Закладывать реалистичность: сеансы, данные, «дум-таймы», вариативность.
Антипаттерн 6. «40 барабанов клавиатуры»
Симптом. Во «фреймворке» десятки однотипных методов: press_down_arrow_key, press_up_arrow_key, press_left_arrow_key, press_right_arrow_key, press_enter_key, press_tab_key, press_backspace_key, press_delete_key, press_space_key, press_char_key, press_character_by_character… Все делают одно и то же: строят ActionChains, жмут клавишу n раз и ещё подсыпают time.sleep(.1) между нажатиями.
Плохой пример (сокращённо)
Что здесь не так?
- Дребезг и flaky. Ручные sleep(.1) — это гадание на таймингах. На CI/других машинах поведение будет разным.
- Дублирование. Десятки почти одинаковых функций → тяжело поддерживать/менять.
- Не по-пользовательски. В E2E мы проверяем сценарии пользователя. Он кликает по видимым элементам; «стрелочками вниз» скроллит редко.
- Преждевременная низкоуровневость. Нажатия клавиш — последняя надежда, когда нет нормальных локаторов/методов.
- Нарушение ожиданий. Нет явных ожиданий состояния (элемент появится/станет кликабельным), только «жми и надейся».
Как правильно (Selenium)
1) Убрать зоопарк — оставить один универсальный хелпер
Использование:
Но пользоваться им только когда без клавиатуры никак (например, нативный выпадающий список).
2) Предпочитать действия через локаторы и ожидания
Использование:
3) Для «прокрутки» — скролл к элементу, а не «стрелки»
И затем клик/взаимодействие по локатору с ожиданием.
Как правильно (Playwright — ещё короче и надёжнее)
Playwright сам делает auto-wait и умеет работать клавиатурой точечно, без ручных sleep.
Когда клавиатура уместна?
- Нативные элементы/меню, которые реально управляются стрелками/Tab у живого пользователя.
- Доступность (a11y): проверка навигации по Tab/Shift+Tab.
- Ввод в поля с масками, где «вклейка» файлами/JS не катит.
Даже в этих случаях минимизируем ручные паузы — лучше дождаться состояния (фокус, видимость, enabled).
Мини-чеклист
- Сначала локаторы + ожидания, потом клавиатура как исключение.
- Один универсальный press() вместо десятка копий.
- Никаких «магических» sleep(.1) внутри хелпера — ждём состояния.
- По возможности — Playwright: меньше кода, больше стабильности.
- Не эмулируем «скролл стрелками» для доставки элемента в вьюпорт; скроллим к элементу и кликаем.
Антипаттерн 7. Загрузка файла «через JS под капотом»
Симптом. Вместо нормальной загрузки файла «фреймворк» вручную «включает» скрытый <input type="file"> и пробует присвоить ему путь строкой через JS:
Что здесь не так?
- Браузерная безопасность. Современные браузеры запрещают устанавливать value у input[type=file] через JS. Это сознательное ограничение безопасности. Такой «трюк» либо не сработает, либо сломается при первом же обновлении.
- Ломает приложение. Насильная правка display/disabled меняет DOM и состояние виджетов, из-за чего падают обработчики, валидации, стили. Тест больше не имитирует пользователя.
- Flaky/нестабильность. Чуть другой CSS/фреймворк — и магия перестаёт работать.
- Отсутствие кросс-браузерности. То, что «завелось» в Chromium, часто не работает в Firefox/Safari.
Как правильно (Selenium)
1) Классика: send_keys() на input[type=file]
Selenium «умеет» загрузки — просто передайте абсолютный путь:
Примечание: если инпут disabled или реально недоступен, надо кликнуть кнопку/label, которая открывает системный диалог — но сам файл всё равно передаём через send_keys по элементу input, а не через JS.
2) Скрытый (aria-виджеты)
Иногда <input type="file"> скрыт, а UI — это кастомная кнопка/лейбл. Делайте так:
Не трогайте display/disabled напрямую — дайте приложению само перевести input в «интерактив».
3) Selenium Grid / удалённый драйвер
На удалённом драйвере нужно включить FileDetector, иначе путь будет «на вашей машине», а не на ноде:
4) Drag&Drop-виджеты (dropzone)
Если фронт принимает файл только через «перетаскивание», у вас два варианта:
- Обойти UI и бить в API загрузки напрямую (предпочтительно для интеграционных тестов).
- Или сымитировать drop-события. В Selenium это громоздко. Честнее здесь использовать Playwright.
Как правильно (Playwright — проще и стабильнее)
Playwright решает загрузки «из коробки» и не требует делать элемент видимым:
Для dropzone-виджетов часто достаточно всё равно указать реальный input по селектору — фреймворки держат его в DOM. Если нет — Playwright поддерживает page.dispatch_event('selector', 'drop', data) или используйте API-загрузку.
Частые грабли и как их обойти
- Относительные пути. Всегда приводите путь к абсолютному.
- Iframe. Если input внутри iframe — сначала frame = page.frame(name="...") / driver.switch_to.frame(...), потом — загрузка.
- Множественный input. Для нескольких файлов нужен атрибут multiple у input; иначе грузите по одному.
- Антивирус/сети. На CI путь должен существовать на агенте, а не на вашей машине. Подкладывайте файлы в репозиторий/артефакты job’а.
- Валидации фронта. После загрузки проверяйте UI-состояние: превью, имя файла, прогресс, успешный статус, а не только факт «отдал send_keys».
Мини-чеклист
- Загружаем файлы только через send_keys (Selenium) или set_input_files (Playwright).
- Для удалённых раннеров — LocalFileDetector.
- Кликаем по официальным контролам (кнопка/label), не ломая DOM.
- При dropzone — либо API, либо Playwright/сложный сценарий drop-события.
- Не назначаем value у file-input через JS.
- Не меняем стили/disabled у input для «обхода» — это делает тест невалидным и нестабильным.
Антипаттерн 8. Фальшивые HTTP-ответы и статусы 0/310/520
Симптом. В «обёртке» над requests/httpx ловятся любые сетевые исключения, после чего руками создаётся «синтетический» Response() с придуманным status_code — например 0 (нет сети), 310 (слишком много редиректов), 520 («неизвестная ошибка»). Снаружи всё выглядит как «нормальный» HTTP-ответ.
Плохой пример (сокращённо)
Что здесь не так?
- Подмена семантики. 0 — не HTTP-код вообще; 310/520 — нестандартные. Мониторинг, ретраи, SLA/алёрты, либы-клиенты и middleware перестают корректно отличать сетевые исключения от реальных HTTP-ответов сервера.
- Ложная телеметрия. Метрики «ошибок сервера 5xx» вдруг растут из-за таймаутов на клиенте — вы лечите не ту сторону.
- Ломается контроль ошибок. Код, который рассчитывает на raise_for_status()/обработку исключений, получает «успешный вызов с кодом 0/520» и идёт дальше.
- Диагностика в никуда. Потерян стек исключения, не видно, где именно упали DNS/SSL/коннект/таймаут.
- Несовместимость. Ретраи по «кодам 0/520» не работают с стандартными политиками (они ждут исключений, а не выдуманных статусов).
Как правильно?
Вариант A. «Чистый» контракт: либо реальный Response, либо исключение
- На HTTP-уровне возвращаем реальный ответ (200/3xx/4xx/5xx) и при необходимости вызываем raise_for_status().
- На сетевом уровне (таймаут/коннект/DNS/SSL) — не прячем проблему: пробрасываем исключение наружу. Ретраим по типам исключений.
Вариант B. Если нужен «обобщённый» результат — делаем явную модель
Так тестам и прод-коду понятно, что именно случилось: HTTP-ошибка или сеть.
Важные практики!
- Таймауты по умолчанию. Никогда не делайте «вечных» запросов.
- Ретраи по исключениям, а не по «кодам 0/520». Добавляйте jitter/backoff.
- Логируйте отдельными полями: метод, URL, status_code (если есть), тип исключения (если есть), длительность, попытку.
- Не глотайте стек. В логах должен сохраняться traceback сетевой ошибки.
- Контент-тайп/парсинг. Не зовите бездумно .json(); проверяйте заголовок или используйте .is_success и fallback к text.
- Метрики. Разводите «HTTP-ошибки» (4xx/5xx) и «сетевые исключения» (timeout/connect). Это разные SLO и разные владельцы.
Мини-чеклист
- Не подменяем исключения на «ответы» с фальш-кодами.
- Возвращаем реальный Response; сетевые проблемы — как исключения.
- Ретраим по ConnectError/Timeout (backoff + jitter).
- Отдельные метрики/логи для HTTP-ошибок и сетевых исключений.
- json= вместо ручного json.dumps; raise_for_status() там, где нужно.
- Если хочется «универсальный результат» — делаем явную модель, а не придумываем HTTP-коды.
Антипаттерн 9. «Один файл на 3500 строк — это не фреймворк»
Симптом. Вся логика — от UI и API до SQL, VPN и нагрузочного тестирования — собрана в один гигантский файл на 3500 строк. Никаких модулей, никакой архитектуры, просто «куча всего».
Золотая табличка на таком коде могла бы быть такой:
«Работает — не трогай. Сломалось — не починишь».
Плохой пример (упрощённо)
Что здесь не так?
- Нарушение SRP (Single Responsibility Principle). Один файл делает всё сразу. UI ≠ API ≠ SQL ≠ DevOps. Поддерживать невозможно.
- Отсутствие модульности. Нельзя переиспользовать кусок кода в другом проекте: он тянет за собой весь «зоопарк».
- Гигантский технический долг. Любая правка/рефакторинг → риск поломать чужую часть, потому что тесты завязаны на весь комбайн.
- Порог входа. Новичок открывает файл и теряется. Где UI? Где база? Где API? Всё в одной простыне.
- Нет тестируемости. Такой монолит нельзя изолированно покрыть юнит-тестами. Всё связано через глобалы.
Как правильно?
Делим на отдельные модули
- ui.py — обёртки над Playwright/Selenium.
- api.py — клиент на httpx.
- db.py — SQLAlchemy/psycopg2 утилиты.
- load.py — нагрузочные сценарии в Locust.
- tools/ — вспомогательные функции (логирование, парсинг).
Каждый модуль отвечает только за своё.
Собираем архитектуру «фреймворка» как пакет
Пример
Тестам больше не нужно импортировать «всё подряд». Они используют только нужное.
Мини-чеклист
- Никогда не складывать всё в один «бог-файл».
- Делить код на модули: UI, API, DB, нагрузка.
- Использовать стандартные библиотеки (Playwright, httpx, SQLAlchemy, Locust).
- Следовать SRP: один модуль = одна зона ответственности.
- Писать юнит-тесты на утилиты, а не на «комбайн».
Почему это вредно?
На первый взгляд подобные «фреймворки» кажутся простыми и удобными: вызвал статический метод LibUI.click_element_by_xpath(...) — и тест готов. Но это иллюзия простоты, за которую потом приходится очень дорого платить.
Эффект Даннинга–Крюгера в действии
Проблема в том, что авторы подобных решений сами не осознают глубину своих ошибок. Отсюда рождаются неудачные практики и самодельные обёртки/комбайны, хотя индустрия уже давно выработала зрелые инструменты и практики: Playwright для UI, httpx для HTTP, pytest для тестов, locust для нагрузки. Эти библиотеки не нуждаются в обёртках на 3500 строк с костылями и хаками.
Чем это плохо для новичков?
- Формируется ложное представление, что «автоматизация» — это набор случайных статических методов.
- Selenium-антипаттерны (sleep, стрелки вниз, дубли функций) закрепляются как «правильная практика».
- SQL, API и UI смешаны в одном файле → стираются границы ответственности. Студент перестаёт понимать, где UI, где база, а где сервис.
- В реальном проекте такой подход ломается на первом же код-ревью или собеседовании.
- Новичку действительно проще «вызвать метод и забыть», но это не обучение, а прививка неподдерживаемого кода. Потом придётся проходить «детокс»: переучиваться и заново строить мышление.
Что реально происходит?
- Это не библиотека, а несвязанный набор утилит без архитектуры, с дублированием кода и небезопасными конструкциями.
- Слоган «пишите меньше кода» достигается не грамотным дизайном, а тем, что всё завязано на костыли и хаки.
- Технический долг зашивается прямо в головы новичков: они искренне думают, что «так и надо писать тесты».
- Попытка «собрать всё и сразу» приводит к абсурду: UI-обвязка на Selenium, SQL для PostgreSQL/MySQL/SQLite, VPN, API на requests, нагрузка через httpx — всё в одном файле.
На деле мы рассмотрели лишь малую часть. Вся библиотека — это 3500 строк кода, которые проще выбросить и написать с нуля, чем пытаться поддерживать.
Как относиться?
Использовать такие вещи в продакшене или даже на учебном проекте — рискованно. Максимум — рассматривать как «справочник» того, что вообще можно сделать с Selenium или requests, а дальше переписать под задачу точечно.
Заключение
Мы все когда-то писали кривой код. Главное — вовремя от этого отвыкнуть и начать писать правильно. Если у вас есть примеры похожих граблей — поделитесь в комментариях: разберём и добавим в чеклист
Антипаттерны, которые мы рассмотрели, — это не просто «забавные костыли». Это системные ошибки, которые мешают автоматизации развиваться, превращают тесты в источник боли и откладывают технический долг на годы вперёд.
Главная мысль проста: писать автотесты правильно не сложнее, чем писать их неправильно. Разница лишь в том, что «правильные» практики дают надёжный, предсказуемый и поддерживаемый код, а «неправильные» — flaky, хаос и бесконечный рефакторинг.
Если вы только начинаете путь в автоматизации — ориентируйтесь на зрелые инструменты и устоявшиеся подходы:
Они уже решают 90% задач «из коробки» и избавляют вас от необходимости собирать собственный «велосипед на 3500 строк».
И самое важное: автотесты — это код. К нему применимы те же правила, что и к боевому продукту: модульность, читаемость, тестируемость, безопасность. Чем раньше вы это усвоите, тем меньше будет «детокса» в будущем.
Так что если вам попадётся библиотека-«комбайн» с магией и костылями — не спешите радоваться, что «писать тесты стало проще». Скорее всего, это ловушка. Лучше потратить чуть больше времени на освоение правильных практик и писать тесты, за которые не будет стыдно ни вам, ни вашему проекту.
Если после этой статьи вы поймали себя на мысли, что часть примеров вам знакома — не беда. Мы все через это проходили. Главное — научиться писать автотесты так, чтобы за них не было стыдно ни на code review, ни на собеседовании.