Вступление
Автоматизация тестирования — это не только про код и фреймворки, но и про подходы, архитектуру и опыт. Многие начинающие автоматизаторы быстро погружаются в написание тестов, но при этом совершают ошибки, которые кажутся «мелочами» — пока не вырастают в большие проблемы: нестабильные тесты, сложность поддержки или путаницу в результатах.
В этой статье я собрал подборку самых частых ошибок, которые встречаются у начинающих инженеров по автоматизации. Цель этой статьи — не высмеять или «поймать на ошибках», а помочь разобраться: почему эти ошибки вредны и как их избежать.
Если вы только начинаете свой путь в автоматизации — возможно, узнаете себя и сможете избежать этих граблей. А если вы уже опытный инженер — есть шанс найти знакомые ситуации и, может быть, дополнить этот список своими наблюдениями.
Примеры в статье приведены на 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. Ошибка: отсутствие фикстур
У начинающих автоматизаторов часто встречается такой код:
def 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):
...
Почему это важно?
Структура. Когда тестов сотни, классы помогают быстро ориентироваться.
-
Общие фикстуры. Можно подключать фикстуры для всего класса:
@pytest.fixture(autouse=True) def prepare_data(self): ...
Маркировка и запуск. Легче запускать целые группы (
pytest -k TestFeature
), а не отдельные тесты.Отчёты. В 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()
Почему это важно?
Ожидания внутри проверки.
expect()
автоматически дождётся, пока элемент появится, исчезнет или изменится. В то время какassert element.is_visible()
проверяет состояние «здесь и сейчас», что часто приводит к флаки-тестам.Более читаемые ошибки. Если проверка упала,
expect()
выведет понятное сообщение с контекстом: «Ожидали, что элемент будет видим, но он не появился в течение X секунд».Лучшие отчёты. Использование
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
Также можно:
Использовать фабрики или сидинг. Можно заранее «посеять» пользователей (seed data) и работать с ними, не полагаясь на результат других тестов.
Делать тесты атомарными. Каждый тест отвечает только за один сценарий и может быть выполнен независимо от остальных.
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‑клиенты, фикстуры, параметризацию, именованные константы.
Не бойтесь рефакторить свои тесты и учиться у более опытных коллег.
Автоматизация тестирования — это инженерная работа. Чем раньше вы начнёте мыслить как инженер, а не как «просто человек, который пишет тесты», тем быстрее вы вырастете и избежите этих ошибок.
Если хотите подробнее посмотреть примеры кода и готовые проекты, рекомендую эти статьи:
API автотесты на Python с запуском на CI/CD и Allure отчетом
UI автотесты на Python с запуском на CI/CD и Allure отчетом. PageObject, PageComponent, PageFactory
Если вы знаете ещё ошибки, которые встречаются у новичков — поделитесь ими в комментариях. Возможно, дополню список вашим опытом.
ahdenchik
Кликбейтный заголовок: в широком смысле, "автоматизаторами" называют тех, кто автоматизирует бизнес