Наверняка многие сталкивались с ситуацией, когда в пятницу вечером падает CI‑пайплайн? Вы открываете лог, видите красный тест, перезапускаете его... и он проходит. Какой‑то сбой думаете вы. Через неделю таких странных тестов становится все больше. А еще через месяц команда перестает верить в автотесты.
Если вам знакома подобная ситуация, то вас можно «поздравить» — вы столкнулись с последствием отсутствия архитектуры в тестовом фреймворке.
Давайте для начала разберемся, зачем вообще автотестам нужна архитектура? Разве недостаточно просто написать тесты? Многие считают, что автоматизация — это просто код, который дергает UI или API. Но когда тестов становится 100, 500, 1000 — отсутствие структуры превращает их в неуправляемый монолит.
Хорошая архитектура тестового фреймворка решает три ключевые задачи:
Стабильность — тест не должен падать из‑за того, что селектор поменялся в одном месте, а не в другом, или из‑за race condition.
Масштабируемость — возможность добавить 200 новых тестов без переписывания старых и увеличения времени выполнения в 10 раз.
Поддержка — новый разработчик должен понять логику за день, а не за неделю.
Отсюда главная метафора: Тест без архитектуры — это одноразовый скрипт. Фреймворк с архитектурой — это промышленный станок.
|
А теперь проверьте, готовы ли вы вообще к построению такой архитектуры. |
Базовые подходы к построению архитектуры
В тестовой автоматизации классические принципы SOLID и паттерны проектирования работают так же хорошо, как и в разработке.
Начнем с разделения на слои (Layered Architecture). Здесь самый надежный подход — отделить «что тестируем» от того «как тестируем». Ниже представлена таблица, в которой указаны слои, зоны ответственности и примеры вызова процедур.
Слой |
Ответственность |
Пример |
Тесты |
Сценарии, данные, assert«ы » |
test_user_can_login() |
Бизнес‑логика (Page Objects / Services) |
Действия над системой |
Действия над системой LoginPage.enter_credentials() |
Драйверы / Клиенты |
Взаимодействие с браузером, БД, API |
driver.find_element() |
Инфраструктура |
Логирование, отчеты, конфиги |
logger.info(), config.get_env() |
Помимо этого, необходимо использовать паттерны, которые реально работают. Вот список основных паттернов, которые можно использовать:
Page Object Model (POM) — стандарт для UI. Одна страница — один класс. Изменение локатора меняется в одном месте.
Screen / Component Object — для сложных интерфейсов с виджетами (дашборды, модалки).
Builder — для генерации тестовых данных с валидными значениями по умолчанию.
Factory — для создания разных состояний объекта (например, UserFactory.create_verified_user()).
Dependency Injection — передача зависимостей (конфиг, логгер, API‑клиент) через конструктор, а не глобальные переменные.
В результате мы получаем готовые паттерны, позволяющие эффективно решать практические задачи.
Давайте, для лучшего понимания рассмотрим несколько примеров реализации: от плохого к хорошему. Начнем с анти‑примера «Простыня».
def test_login(): driver = webdriver.Chrome() driver.get("https://example.com") driver.find_element(By.ID, "username").send_keys("test") driver.find_element(By.ID, "pass").send_keys("123") driver.find_element(By.ID, "login_btn").click() time.sleep(2) assert "Welcome" in driver.page_source driver.quit()
Здесь любому разработчику, который знаком с рекомендациями по написанию качественного кода на Python будет очевидно, что в коде жестко приколочены передаваемые значения, что не очень хорошо. Помимо невозможности переиспользовать логин, применение time.sleep является злом, так как мы просто фиксируем некоторый временной интервал, за который страница должна быть открыта. Если за это время она не откроется, мы получим сбой теста, хотя на самом деле с контентом может быть все в порядке, просто небольшая задержка в сети. Ну и представьте, что у вас 500 таких тестов. При смене ID нужно править их все!
Теперь давайте посмотрим хороший пример:
# pages/login_page.py class LoginPage: def init(self, page: Page): self.page = page self.username_input = page.locator("#username") self.password_input = page.locator("#password") self.login_button = page.locator("#login_btn") def login_as(self, user: User) -> DashboardPage: self.username_input.fill(user.name) self.password_input.fill(user.password) self.login_button.click() return DashboardPage(self.page) # test_auth.py def test_successful_login(authenticated_dashboard): assert authenticated_dashboard.is_loaded()
Здесь у нас нет недостатков из предыдущего примера. Так, мы не используем time.sleep — ожидания встроены в locator. А User — это фикстура с валидными данными. Результат выполнения действия представлен в authenticated_dashboard, что правильнее поиска конкретного текстового вхождения, как в предыдущем примере.
Работа с конфигурациями и разными окружениями
Одна из главных причин «красных тестов на CI, но зеленых локально» — неправильная работа с окружениями. Здесь хардкод также вреден, как и в программировании.
BASE_URL = "https://dev.myapp.com" # А если prod? staging? API_KEY = "sk-123456" # В коде? Серьезно?
Это тот случай, когда жесткое приколачивание гвоздями, это не просто плохо, но и опасно.
В хорошем примере мы воспользуемся подходом 12 факторов. Прежде всего, примените иерархиею:
Значения по умолчанию (defaults)
Файлы окружений (config/dev.yaml, config/staging.yaml)
Переменные окружения (их приоритет выше всего)
Секреты — никогда в коде, только через Vault / GitHub Secrets / AWS Secrets Manager.
Вот пример структуры (pydantic‑settings или dynaconf):
# config/staging.yaml app: base_url: "https://staging.myapp.com" api_url: "https://api.staging.myapp.com" db: host: "test-db.internal" timeout: 30
Код на python:
# config.py from dynaconf import Dynaconf settings = Dynaconf( envvar_prefix="MYTESTS", settings_files=["config/default.yaml", f"config/{os.getenv('ENV')}.yaml"], )
Запуск:
|
ENV=staging MYTESTS_API_KEY=xxx pytest |
В целом, работая с тестовыми данными на разных окружениях используйте стратегию изоляции:
Для локального запуска — Docker‑контейнер с чистой БД.
Для dev/staging — выделенный tenant или схема с откатом транзакции после каждого теста.
Для prod‑like — только read‑only тесты или моккинг внешних систем.
Лучшая практика: Никогда не запускайте тесты, которые что‑то пишут, на проде. Даже с тестовыми картами. Однажды это приведет к инциденту.
Анти‑паттерны в автотестах
Далее, давайте поговорим об автотестах. Здесь тоже есть свои анти‑паттерны и мы рассмотрим несколько примеров.
1. Chain of Clicks (цепочка кликов)
Наглядный, но плохой пример:
driver.find_element(...).click()
driver.find_element(...).click()
driver.find_element(...).click()
Вроде бы все просто — отдельные клики, но при падении на 3-м клике вы не знаете текущее состояние системы. В таких случаях лучше разбивать на бизнес‑методы — open_menu(), select_filter(), apply().
2. Shared Test Data (общие данные между тестами)
user = create_user(“test@mail.com”)
# тест 1 удаляет user, тест 2 его ищет — падает.
Лучше, когда каждый тест создает свои данные и удаляет их. При необходимости используйте уникальные идентификаторы (UUID в email).
3. Sleep‑зависимость
time.sleep(5)
А почему собственно ждать надо 5 секунд? А может 10 или больше. Здесь лучше всего использовать явные ожидания (WebDriverWait, expect, poll_until).
wait_for_condition(lambda: page.is_visible(“.result”), timeout=10)
4. Глобальный State‑Fixture
Фикстура, которая меняет глобальный конфиг
@pytest.fixture(scope="session") def setup(): change_system_language("ru")
Вносимое изменение очевидно повлияет на все тесты! Лучше заменить на Fixture с scope=«function» или восстановление состояния после теста (yield + revert).
5. God Object в Page Object'е
class MainPage: def do_everything(self): # 200 методов внутри ...
В этом случае используйте декомпозицию на компоненты: HeaderMenu, ShoppingCart, ProductGrid.
Заключение
Архитектура тестового фреймворка — это не «лишняя работа», а инвестиция в спокойствие команды. Она не появляется сама собой, ее нужно проектировать с первого дня. Здесь многие могут возразить, что все это слишком громоздко, а ведь мне «только протестить». Но на самом деле зачастую небольшой набор простых тестов в итоге начинает все больше обрастать все новыми, более запутанными проверками и уже никто не хочет может все это привести в нормальный вид.
Подводя итог, начните с малого: выделите слой Page Objects или API‑клиентов, затем вынесите все URL и ключи в конфиги, замените sleep на явные ожидания. Затем, напишите один тест по всем правилам, а после того, как его отладите, перепишите остальные. И тогда ваши автотесты перестанут быть источником головной боли и станут надежной сеткой безопасности, которая ловит баги, а не флапает на ровном месте.

Прочитали статью про архитектуру тестов? Поняли, почему time.sleep и God Object ведут к красным CI-пайплайнам в пятницу?
Теперь — практика.
На курсе «Автоматизатор тестирования на Python» вы превратите теорию из статьи в промышленный фреймворк: Page Objects, явные ожидания, работу с окружениями, изоляцию данных и отчёты, в которые не стыдно смотреть.
Ваши тесты перестанут флапать. Команда снова поверит в автоматизацию.