15 типичных ошибок начинающих автоматизаторов (и как их избежать)

15 типичных ошибок начинающих автоматизаторов (и как их избежать)

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

Вступление

Автоматизация тестирования — это не только про код и фреймворки, но и про подходы, архитектуру и опыт. Многие начинающие автоматизаторы быстро погружаются в написание тестов, но при этом совершают ошибки, которые кажутся «мелочами» — пока не вырастают в большие проблемы: нестабильные тесты, сложность поддержки или путаницу в результатах.

В этой статье я собрал подборку самых частых ошибок, которые встречаются у начинающих инженеров по автоматизации. Цель этой статьи — не высмеять или «поймать на ошибках», а помочь разобраться: почему эти ошибки вредны и как их избежать.

Если вы только начинаете свой путь в автоматизации — возможно, узнаете себя и сможете избежать этих граблей. А если вы уже опытный инженер — есть шанс найти знакомые ситуации и, может быть, дополнить этот список своими наблюдениями.

Примеры в статье приведены на Python, но описанные подходы и принципы одинаково применимы и к другим языкам программирования.

1. Ошибка: один тест — одна проверка

Одна из первых «мантр», которые слышат начинающие автоматизаторы:

«Один тест — одна проверка»

Идея правильная: тест должен быть понятным и легко диагностируемым. Когда тест проверяет слишком много разных вещей, его падение превращается в квест: «а что именно пошло не так?»

Однако эту идею часто воспринимают слишком буквально:

def test_feature_check_title(): login_page.check_title() def test_feature_check_email(): login_page.check_email() def test_feature_check_password(): login_page.check_password()

Формально — да, по одной проверке на тест. Но по сути это одна и та же функциональность (например, валидация формы логина), искусственно разорванная на отдельные кусочки. В итоге:

  • тестов становится больше, чем нужно;
  • поддержка усложняется;
  • запуск тестов становится дольше;
  • отчёты захламляются, а анализ результатов замедляется.

На практике важно другое: один тест — один сценарий или одна фича. Если логика проверки связана и естественным образом выполняется последовательно, её можно объединить:

def test_login_form_fields(): login_page.check_title() login_page.check_email() login_page.check_password()

Если этот тест упал — значит, что-то в сценарии «форма логина» работает неправильно. И это нормально: отладить причину проще, чем поддерживать три отдельных теста, которые проверяют одно и то же «флоу».

Вывод:

  • Делите тесты по сценариям, а не по отдельным строчкам assert’ов.
  • Если проверки логически связаны — оставляйте их в одном тесте.
  • Если проверки независимы (например, разные фичи) — выносите их в отдельные тесты.

2. Ошибка: отсутствие test id

Одна из самых частых ошибок у начинающих автоматизаторов — игнорирование test id и привязка к хрупким селекторам:

class LoginPage: def __init__(self, page): self.email_input = page.locator("//div//div//input[@class='...']") self.password_input = page.locator("//div//div//input[@class='...']")

На первый взгляд — всё работает. Но как только дизайнер или фронтендер поменяет структуру или класс, тесты начнут падать. Это не баг продукта, а проблема теста — он оказался слишком хрупким.

Современный подход — использование специальных атрибутов для тестирования (data-testid, automation-id, и т.п.). Они не зависят от внешнего вида и верстки:

class LoginPage: def __init__(self, page): self.email_input = page.get_by_test_id("login-page-email-input") self.password_input = page.get_by_test_id("login-page-password-input")

Почему это важно:

  • тесты становятся стабильнее;
  • изменения UI не ломают сценарии;
  • тесты быстрее пишутся и проще поддерживаются.

Вывод: используйте test id, а не хрупкие XPath и CSS селекторы — это простой способ сделать ваши тесты более надёжными и «живучими» в долгосрочной перспективе.

3. Ошибка: Allure-шаги внутри теста

Новички часто начинают использовать Allure прямо внутри тестов:

def test_create_user(): with allure.step('Create new user'): users_client.create_user() def test_delete_user(): with allure.step('Create new user'): user = users_client.create_user() with allure.step('Delete user'): users_client.delete_user(user.id)

На первый взгляд это выглядит удобно — отчёт получается красивым и понятным. Но на практике, когда тестов становится сотни или тысячи, дублирование шагов внутри каждого теста превращается в боль. Если завтра поменяется логика или название шага — менять придётся во всех тестах вручную.

Как лучше?

Вынесите шаги на уровень клиента и используйте их один раз:

class UsersClient: @allure.step('Create new user') def create_user(self): ... @allure.step('Delete user') def delete_user(self, user_id): ...

Теперь тесты становятся чистыми и читаемыми:

def test_create_user(users_client): users_client.create_user() def test_delete_user(users_client): user = users_client.create_user() users_client.delete_user(user.id)

Что мы выиграли:

  • шаги отображаются в отчёте Allure автоматически;
  • тесты не захламлены техническими деталями;
  • поддержку и изменения делать проще: если меняется логика — правим в одном месте.

4. Ошибка: отсутствие API клиентов

Частая ошибка начинающих автоматизаторов — обращаться к API напрямую прямо из тестов:

def test_get_user(): response = lib.get("http://localhost:8000/users/1") assert response.status_code == OK

На маленьком проекте это кажется удобным и быстрым: зачем писать ещё один класс, если и так работает? Но в перспективе это создаёт серьёзные проблемы:

  • URL и эндпоинты размазаны по тестам;
  • при изменении API нужно править десятки или сотни тестов;
  • тесты становятся «грязными» и плохо читаются.

Как лучше?

Используйте API‑клиенты. Да, кода изначально будет чуть больше, но в будущем это экономия времени и нервов:

class UsersClient: def get_user(self): return lib.get("http://localhost:8000/users/1")

Теперь тест выглядит чище:

def test_get_user(): client = UsersClient() response = client.get_user() assert response.status_code == OK

Преимущества:

  • при изменении URL или параметров правим в одном месте;
  • тесты читаются как бизнес‑сценарии, а не как набор технических вызовов;
  • проще добавить логи, обработку ошибок и Allure‑шаги внутри клиента, не трогая сами тесты.

5. Ошибка: отсутствие PageObject

Одна из классических ошибок новичков в UI‑автоматизации — писать селекторы и проверки прямо внутри тестов:

def test_feature(): email_input = page.get_by_test_id("login-page-email-input") password_input = page.get_by_test_id("login-page-password-input") expect(email_input).to_be_visible() expect(password_input).to_be_visible()

На первый взгляд это удобно: всё на месте, тест сразу виден целиком. Но через пару месяцев проект разрастается, появляются десятки и сотни тестов — и в каждом из них повторяются одни и те же селекторы и проверки. Любое изменение верстки превращается в боль: приходится править десятки тестов вручную.

Как лучше — PageObject

Используйте PageObject — выделяйте логику взаимодействия со страницей в отдельные классы:

class LoginPage: def __init__(self, page): self.email_input = page.get_by_test_id("login-page-email-input") self.password_input = page.get_by_test_id("login-page-password-input") def check_visible(self): expect(self.email_input).to_be_visible() expect(self.password_input).to_be_visible()

Тест становится чище и лучше читается:

def test_feature(page): login_page = LoginPage(page) login_page.check_visible()

Преимущества:

  • изменения в верстке правятся в одном месте (в PageObject);
  • тесты читаются как бизнес‑сценарии, а не как технический набор команд;
  • легче добавлять дополнительную логику (логи, шаги, ожидания) без изменения тестов.

6. Ошибка: отсутствие параметризации

Эта ошибка встречается очень часто у начинающих автоматизаторов. Обычно выглядит она примерно так:

def test_feature1(): login_page = LoginPage() login_page.check_login_form(email="user@mail.com", password="one") def test_feature2(): login_page = LoginPage() login_page.check_login_form(email="user@gmail.com", password="two") def test_feature3(): login_page = LoginPage() login_page.check_login_form(email="user@inbox.com", password="three")

А иногда ещё хуже — тест пишется один, а входные данные гоняются в цикле:

def test_feature(): for email, password in [ ("user@mail.com", "one"), ("user@gmail.com", "two"), ("user@inbox.com", "three"), ]: login_page = LoginPage() login_page.check_login_form(email=email, password=password)

На первый взгляд кажется, что так проще — но результат получается не очень:

  • если что-то падает, вы не знаете, на каких именно данных;
  • отчёт о тестировании становится нечитаемым (в отчёте это будет один тест);
  • сложнее управлять входными данными.

Как правильно?

Используйте встроенные возможности параметризации тестов, например в pytest:

import pytest @pytest.mark.parametrize("email, password", [ ("user@mail.com", "one"), ("user@gmail.com", "two"), ("user@inbox.com", "three"), ]) def test_feature(email, password): login_page = LoginPage() login_page.check_login_form(email=email, password=password)

Так каждый набор данных становится отдельным тестом:

  • упал конкретный вариант → сразу видно, где ошибка;
  • отчёт красивый и читаемый;
  • код лаконичный и поддерживаемый.

7. Ошибка: отсутствие фикстур

У начинающих автоматизаторов часто встречается такой код:

ef test_feature1(): login_page = LoginPage() login_page.check_visible() def test_feature2(): login_page = LoginPage() login_page.check_visible()

На первый взгляд всё вроде нормально: тесты выполняются, проверка проходит. Но есть одна проблема — повторение кода. Каждый тест создаёт LoginPage вручную, и таких мест может быть десятки или даже сотни.

Как правильно?

Вместо дублирования нужно использовать фикстуры. Например, в pytest:

import pytest @pytest.fixture def login_page(): return LoginPage() def test_feature1(login_page): login_page.check_visible() def test_feature2(login_page): login_page.check_visible()

Теперь объект LoginPage создаётся одним местом (в фикстуре), а сами тесты стали чище и короче. Если завтра нужно будет, например, добавить авторизацию или настроить браузер для всех тестов — вы делаете это один раз в фикстуре, а не правите сотни тестов.

Бонус: уровни фикстур

  • scope="function" — фикстура создаётся перед каждым тестом (по умолчанию).
  • scope="module" — создаётся один раз на модуль.
  • scope="session" — один раз на всю сессию (например, если нужно поднять базу данных или сервис).

8. Ошибка: использование «магических чисел»

Часто у начинающих автоматизаторов в тестах можно встретить вот такой код:

def test_feature(): response = users_client.get_user() assert response.status_code == 400

Сработает? Да, сработает. Но проблема в том, что «400» здесь непонятно что значит. Через месяц вы откроете тест и будете вспоминать: «А почему именно 400? Что мы тут проверяли? Это не найденный пользователь или ошибка валидации?»

Как правильно?

Вместо «магических чисел» используйте именованные константы, например, из стандартной библиотеки http:

from http import HTTPStatus def test_feature(): response = users_client.get_user() assert response.status_code == HTTPStatus.BAD_REQUEST

Теперь тест читается без лишних пояснений. По коду сразу понятно, что мы ожидаем ошибку в запросе, а не какой-то «мистический 400». А если в будущем поменяется код (например, сервис начнёт возвращать 422), вы сможете легко найти все такие проверки и обновить их.

Вывод

  • Используйте именованные константы (HTTPStatus, свои enum’ы или словари с кодами).
  • Избегайте «магических» значений в коде: это делает тесты понятнее и поддерживаемее.

9. Ошибка: отсутствие тестовых классов или сьютов

Часто встречается ситуация, когда все тесты пишутся «плоско», без объединения в классы или тестовые сьюты:

@pytest.mark.smoke def test_feature1(): ... @pytest.mark.smoke def test_feature2(): ... @pytest.mark.smoke def test_feature3(): ...

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

Как правильно?

Объединяйте тесты по смыслу в классы (сьюты). Например, если тесты относятся к одной функциональности, лучше оформить их так:

@pytest.mark.smoke class TestFeature: def test_feature1(self): ... def test_feature2(self): ... def test_feature3(self): ...

Почему это важно?

  1. Структура. Когда тестов сотни, классы помогают быстро ориентироваться.
  2. Общие фикстуры. Можно подключать фикстуры для всего класса:
@pytest.fixture(autouse=True) def prepare_data(self): ...
  1. Маркировка и запуск. Легче запускать целые группы (pytest -k TestFeature), а не отдельные тесты.
  2. Отчёты. В Allure и аналогах структура тестов отображается более читаемо.

Вывод

  • Объединяйте тесты в классы/сьюты.
  • Старайтесь, чтобы каждый класс тестировал одну конкретную область.
  • Это сделает проект понятнее и поддерживаемее.

10. Ошибка: использование assert вместо expect

Одна из частых ошибок — использовать обычные Python-ассерты для проверки UI-элементов:

def test_feature(): element = page.locator("...") assert element.is_visible()

На первый взгляд всё нормально — проверка ведь работает. Но у Playwright (и некоторых других UI-фреймворков) есть собственный механизм проверок, специально заточенный под работу с асинхронным UI и ожиданиями:

def test_feature(): element = page.locator("...") expect(element).to_be_visible()

Почему это важно?

  1. Ожидания внутри проверки. expect() автоматически дождётся, пока элемент появится, исчезнет или изменится. В то время как assert element.is_visible() проверяет состояние «здесь и сейчас», что часто приводит к флаки-тестам.
  2. Более читаемые ошибки. Если проверка упала, expect() выведет понятное сообщение с контекстом: «Ожидали, что элемент будет видим, но он не появился в течение X секунд».
  3. Лучшие отчёты. Использование expect() даёт более структурированную информацию в отчётах (например, в Allure).

Вывод

  • Для UI-тестов всегда используйте expect, а не «голый» assert.
  • assert хорошо подходит для чистых Python-проверок (например, числовых значений), но не для элементов UI.

11. Ошибка: хардкод значений вместо настроек

Иногда в коде тестов можно встретить что-то вроде:

def test_feature(): response = lib.get("http://localhost:8000/users/1") assert response.status_code == OK

На первый взгляд, это работает. Но жёстко зашитые значения (http://localhost:8000) создают серьёзные проблемы:

  • поменялся домен или порт → придётся править десятки тестов;
  • одно окружение для разработчиков, другое для CI → тесты ломаются;
  • сложнее масштабировать проект на разные стенды.

Как делать правильно?

Выносите такие значения в конфигурацию:

class Settings: base_url = "http://localhost:8000"

А в тестах используйте их:

def test_feature(settings): response = lib.get(f"{settings.base_url}/users/1") assert response.status_code == OK

Дополнительно

  • В реальных проектах используют не просто класс, а файлы настроек (.env, config.yaml) и библиотеки вроде pydantic или dynaconf.
  • Это позволяет менять окружение одной переменной, без переписывания тестов.

12. Ошибка: хранение чувствительных данных в коде

Иногда встречается примерно такой код:

def test_login(): response = lib.post( "http://localhost:8000/api/login", json={"username": "admin", "password": "SuperSecret123!"} ) assert response.status_code == OK

На первый взгляд — удобно: всё в одном месте, тест запускается «из коробки». Но это плохая практика по нескольким причинам:

  • пароль хранится прямо в коде → он попадёт в репозиторий;
  • при смене пароля придётся править код;
  • если проект открытый (Open Source) или используется внешний CI/CD, данные могут утечь.

Как делать правильно?

Храните данные в переменных окружения:

export TEST_USER=admin export TEST_PASSWORD=SuperSecret123!

Подключайте их через настройки (например, Pydantic Settings):

from pydantic_settings import BaseSettings class Settings(BaseSettings): username: str password: str settings = Settings() def test_login(): response = lib.post( "http://localhost:8000/api/login", json={"username": settings.username, "password": settings.password} ) assert response.status_code == 200

Или храните секреты в безопасном месте (Vault, Doppler, 1Password CLI).

Резюме

Никогда не храните пароли, токены или ключи прямо в коде. Это не только риск утечек, но и лишняя боль при поддержке тестов.

13. Ошибка: асинхронность без надобности

Что обычно происходит? Начинающий автоматизатор видит современный Python и думает: «Асинхронность — это же круто, значит надо делать всё async!» В итоге появляются такие тесты:

import pytest @pytest.mark.asyncio async def test_get_user(): response = await lib.get("http://localhost:8000/users/1") assert response.status_code == OK

В чём проблема?

  • Асинхронность добавляет сложность: нужны event-loop, поддержка в фреймворке, особая отладка.
  • Но выгоды нет: запрос всё равно выполняется последовательно, тесты не становятся быстрее, зато появляются проблемы с совместимостью библиотек.
  • Новичок тратит время на борьбу с RuntimeError: Event loop is closed, Task was destroyed but it is pending! и другими сюрпризами.

Как делать правильно?

Используйте асинхронность только там, где она реально нужна:

  • если вы тестируете нативно асинхронное API (например, websockets);
  • если библиотека тестирования или клиент изначально асинхронные (например, httpx.AsyncClient или playwright.async_api).

Во всех остальных случаях — обычный синхронный код проще и надёжнее:

def test_get_user(): response = lib.get("http://localhost:8000/users/1") assert response.status_code == OK

14. Ошибка: связанные автотесты

Как выглядит на практике? Часто можно встретить такие тесты:

user_id = None def test_create_user(): global user_id user_id = users_client.create_user().id assert user_id is not None def test_delete_user(): response = users_client.delete_user(user_id) assert response.status_code == 200

Здесь test_delete_user зависит от того, что test_create_user отработал успешно. Если первый тест упадёт — второй даже не имеет смысла запускать.

Почему это плохо?

  • Автотесты должны быть изолированными: каждый тест можно запустить в любом порядке, хоть один, хоть все сразу.
  • Такой подход ломает параллельный запуск: тесты начинают «драться» за общие данные.
  • Если нужно прогнать только один тест (например, test_delete_user) — придётся запускать и test_create_user.

Как делать правильно?

Подготавливать данные внутри каждого теста или через фикстуры:

@pytest.fixture def user_id(users_client): return users_client.create_user().id def test_delete_user(user_id, users_client): response = users_client.delete_user(user_id) assert response.status_code == 200

Также можно:

  1. Использовать фабрики или сидинг. Можно заранее «посеять» пользователей (seed data) и работать с ними, не полагаясь на результат других тестов.
  2. Делать тесты атомарными. Каждый тест отвечает только за один сценарий и может быть выполнен независимо от остальных.

15. Ошибка: использование «сырых данных» вместо моделей

Начинающие автоматизаторы часто передают данные напрямую в виде словарей:

def test_create_user(): response = client.post("/users", json={ "name": "John", "age": 30, "email": "john@example.com" }) assert response.status_code == OK

На старте это кажется простым и быстрым решением. Но со временем проект разрастается:

  • поля меняются;
  • появляются вложенные структуры;
  • кто-то случайно опечатался в ключе — и тест падает не там, где ожидаешь.

Как лучше?

Использовать модели данных:

  • Pydantic (рекомендуется для сложных структур и валидации);
  • dataclasses (как минимум, для читаемости и явной структуры).
from pydantic import BaseModel class User(BaseModel): name: str age: int email: str def test_create_user(): user = User(name="John", age=30, email="john@example.com") response = client.post("/users", json=user.model_dump()) assert response.status_code == OK

Преимущества:

  • автопроверка типов и обязательных полей;
  • меньше опечаток и «магических ключей»;
  • код тестов становится читабельнее и надёжнее.

Вывод:

Не передавайте «сырые словари» в коде. Используйте хотя бы dataclass, а лучше Pydantic-модели — это уменьшит количество скрытых ошибок и упростит поддержку тестов.

Вывод

Ошибки, которые делают начинающие автоматизаторы, чаще всего связаны не с языком программирования или выбором фреймворка, а с подходами и архитектурой. Даже идеально написанный тест не принесёт пользы, если он хрупкий, связан с другими тестами или требует правок в десятках мест при любом изменении продукта.

Главная мысль проста:

  • Старайтесь писать тесты так, чтобы их было легко поддерживать.
  • Используйте подходы и практики, которые давно зарекомендовали себя: PageObject, API‑клиенты, фикстуры, параметризацию, именованные константы.
  • Не бойтесь рефакторить свои тесты и учиться у более опытных коллег.

Автоматизация тестирования — это инженерная работа. Чем раньше вы начнёте мыслить как инженер, а не как «просто человек, который пишет тесты», тем быстрее вы вырастете и избежите этих ошибок.

Если хотите подробнее посмотреть примеры кода и готовые проекты, рекомендую эти статьи:

Начать дискуссию