Как стать автором
Обновить
66.59
SimbirSoft
Лидер в разработке современных ИТ-решений на заказ

Как создать тестовый фреймворк с нуля на Playwright

Уровень сложностиСредний
Время на прочтение22 мин
Количество просмотров2.2K

Меня зовут Роман. Я SDET-специалист в компании SimbirSoft. В этой статье поделюсь своим опытом создания тестового фреймворка с нуля для одного из наших внутренних проектов. Материал будет полезен для начинающих или уже действующих специалистов в области тестирования, которые хотят больше узнать: 

  • о построении процесса автоматизации с самого начала; 

  • о сложностях, с которыми может столкнуться автоматизатор;

  • об инструментах для подходящего решения поставленных перед ним задач.

Также рекомендую свою статью тем специалистам, которые уже обладают базовыми знаниями Python и Docker и стремятся углубить свои навыки в автоматизации тестирования.

Содержание

Введение

Обзор технологий

Настройка инфраструктуры

Структура проекта

• Driver и BaseElement

• Page object

• UI-тесты

• API-тесты

Заключение

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

Поскольку проект активно развивался в течение последнего года, то QA-специалисту приходилось тратить больше времени на проведение регрессионных тестов после каждого релиза. Решение этой проблемы было предложено мне. Задача мне сразу показалась заманчивой, ведь разработка фреймворка с нуля – это возможность попробовать новые инструменты, приобрести новые знания и навыки. Не долго думая, я взялся за реализацию собственных идей.

Задача тестирования на проекте с самого начала обещала быть непростой: до того как я присоединился к команде, вся тестовая документация хранилась исключительно в голове единственного QA-специалиста. Систематического тестирования на проекте не было, новый функционал проверялся «на лету». Это порождало ряд вопросов: возможно и целесообразно ли внедрение автоматизации в рамках проекта, как разработать комплекс тест-кейсов без наличия формальной документации и каковы ожидания от процесса автоматизации в целом? Все это вопросы, не имеющие однозначного ответа.

Обоснование внедрения автоматизации подкрепляется теорией тестирования в следующих случаях:

  • Проекту больше года, имеется обширный пул тест-кейсов, на их проверку уходит значительное время.

  • В команде несколько разработчиков, и автоматизация тестирования способствует верификации того, что изменения в коде не повлияли на чужой код.

  • Тестирование разных версий продукта.

  • Проект характеризуется частыми релизами и на генерацию тестовых данных занимает много времени.

Проект соответствовал двум из этих критериев: он достаточно зрелый, с командой из пяти разработчиков, релизы выходят еженедельно. Внедрение автоматизации тестирования уже на данном этапе приносило улучшения – это позволило сократить время на регрессионное тестирование и уменьшить задержки между выявлением дефектов и их исправлением. Сэкономленное время на регрессе составило около 5 часов, а с учетом 85% покрытия регрессионного набора тест-кейсов оставшиеся 15% тест-кейсов оказались слабо подвержены автоматизации и продолжили выполняться вручную.

Однако ключевым аспектом оставался вопрос пользы автоматизации для бизнеса: если ошибка может привести к значительным финансовым потерям, бизнес более склонен к внедрению процесса автоматизации. В контексте нашего проекта – платформы управления персоналом – стоимость ошибки невелика, но возможность ускорить регрессионное тестирование и опробовать новые технологии уже представляла большую ценность.

Первые шаги к автоматизации включали консультации с тестировщиком и сбор тест-кейсов, использованных в регрессе, из которых были выбраны кандидаты на автоматизацию. Затем я применил исследовательское тестирование, моделируя использование системы с точки зрения менеджера, что привело к созданию списка пользовательских сценариев, включая такие операции, как вход в систему, создание и управление сотрудниками. Это обозначило начальное направление для UI-тестирования.

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

Обзор технологий

Теперь перейдем к обзору инструментария, который я выбрал для проведения автоматизации тестирования.

При тестировании на Python без Pytest с его обширным набором киллер-фичей точно не обойтись. Библиотеку для UI-тестирования долго также выбирать не пришлось, коллеги давно уже расхваливали Playwright. Просмотрев несколько обучающих видеороликов, я убедился в его мощи... что ж, наконец и у меня появилась возможность его опробовать. Playwright будет использоваться как для UI, так и для API-тестирования. На проекте используется GitLab, поэтому буду использовать GitLab CI/CD для настройки пайплайна проекта, ну и в конечном счете для упаковки и запуска тестового фреймворка будет использоваться Docker. Для создания тестовых данных задействую Faker, для валидации API-ответов и конфигурации — Pydantic.

Настройка инфраструктуры

Прежде чем приступить к написанию тестов, необходимо настроить рабочее окружение. Будем использовать Docker, во-первых, это позволит обеспечить единообразие инструментария. Не важно, какой браузер установлен или на какой платформе будет происходить разработка, — использование Docker-контейнера обеспечивает одинаковое поведение одних и тех же инструментов как на локальной машине, так и в среде CI. Применение базового образа Playwright от Microsoft с предустановленными браузерами позволит избавиться от необходимости устанавливать и настраивать браузеры вручную, что сэкономит время и минимизирует вероятность ошибок, связанных с несовместимостью версий. Во-вторых, получится передавать переменные окружения в контейнер, позволяя, к примеру, изменять тестовую среду, на которой будут запускаться тесты.

Давайте приступим к написанию Dockerfile. Используем образ Playwright на базе Ubuntu. В образ включены предустановленные браузеры и браузерные зависимости, сам Playwright необходимо устанавливать отдельно. Перейдем к настройке окружения.

Для создания виртуального окружения использую poetry. Этот пакетный менеджер обеспечивает более удобное и точное управление зависимостями с помощью файла poetry.lock, который фиксирует точные версии установленных пакетов и обеспечивает одинаковые зависимости на всех средах. Также из преимуществ poetry можно выделить возможность использования разного набора пакетов на проде и тестовый среде и настройку pytest прямо в конфиге poetry — poetry.toml через директиву [tool.pytest.ini_options]. Перейдем к  установке Poetry с помощью команды:

curl -sSL https://install.python-poetry.org | python -

 Проверим установку менеджера пакетов с помощью команды:

poetry --version

Теперь можно приступить к созданию виртуального окружения командой:

 poetry init

Система предложит ввести информацию, необходимую для инициализации виртуального окружения. Можно использовать значения по умолчанию, нажимая Enter. Далее устанавливаем интересующие нас пакеты командой:

poetry add pytest-playwright pytest pydantic pydantic-settings faker

После того как пакеты установились и сгенерировался lock-файл, перейдем к настройке Docker-образа нашего проекта, создав Dockerfile. Выглядит он следующим образом:

FROM mcr.microsoft.com/playwright/python:v1.49.0-noble
 
ARG DISPLAY
ENV PYTHONUNBUFFERED=1 \
    PYTHONWARNINGS=ignore \
    POETRY_VIRTUALENVS_CREATE=false \
    POETRY_NO_INTERACTION=1 \
    PYTHONPATH="/app:${PYTHONPATH}" \
    DISPLAY=${DISPLAY}

ENV PATH="$POETRY_HOME/bin:/root/.local/bin:$PATH"
RUN pip install --upgrade pip
RUN apt update && apt install -y xvfb x11-utils --no-install-recommends curl \

	&& curl -sSL https://install.python-poetry.org | python - \
    && pip install certifi --upgrade

 
WORKDIR /app
 
COPY pyproject.toml poetry.lock /app/
RUN poetry install --no-interaction --no-ansi

RUN playwright install chrome
COPY . /app
 
CMD ["sh", "-c", "while true; do sleep 3600; done"]

Многое из этого Dockerfile является стандартным для Python-проекта, и наверняка вам уже встречалось. Остановимся подробнее. 

Docker не имеет собственного GUI‑интерфейса для отображения, поэтому необходимо установить виртуальный дисплейный сервер — Xvfb, который позволит нам запустить окно браузера и Playwright отладчик из контейнера. Я работаю на Linux, эта OS как и другие, может работать с несколькими дисплеями, который может быть как физическим, так и удаленным (например, через SSH). В нашем случае дисплей виртуальный, поэтому необходимо его определить, добавив системную переменную DISPLAY. В Windows DISPLAY как переменная окружения не используется, поскольку архитектура графической системы Windows отличается от X Window System, которая характерна для Unix-подобных систем. Поэтому необходимо использовать такую программу, как MobaXterm.

Для демонстрационных целей буду использовать Chrome, но также можно установить Firefox или Webkit. Последняя команда необходима для того, чтобы контейнер не завершался при запуске, это позволит выполнять команды изнутри контейнера.

 Чтобы упростить управление фреймворком, создам файл docker-compose.yml, который будет использоваться только для локальной разработки. Вот как он выглядит:

services:
  tests:
    restart: always
    build:
      context: .
      dockerfile: ./Dockerfile
    volumes:
      - .:/app:delegated
      - /tmp/.X11-unix:/tmp/.X11-unix
    env_file:
      - .env
    network_mode: host

В представленном конфиге описан сервис tests, используемый для запуска автотестов в контейнере. В директиве build задаём текущую директорию доступной для сборки и указываем путь до Dockerfile. Следующей директивой volumes монтируем текущую директорию с хоста в директорию /app внутри контейнера с флагом delegated. Этот флаг ускоряет работу при синхронизации файлов, позволяя IDE оперативно отображать изменения внутри контейнера. С помощью /tmp/.X11-unix:/tmp/.X11-unix прокидываем сокет X11 с хоста в контейнер, это необходимо для работы графического сервера Xvfb внутри контейнера. Последняя директива позволяет контейнеру использовать сетевое пространство имен хоста, что упрощает взаимодействие локально развернутых приложений и тестов.

Структура проекта

Организация структуры проекта играет ключевую роль в его поддержке, читаемости, удобстве использования и масштабируемости. Предлагаю рассмотреть ее.

Ядро проекта находится в директории framework. В ней расположены модули с описанием базовых и списковых элементов, объект Driver, и базовые фикстуры, требующиеся для работы  тестов. Эти элементы будет рассмотрены подробнее позже. 

В директории common располагаются общие компоненты, которые используются в обоих видах тестов (API и UI). Например, здесь можно найти API-клиент, SSH-клиент и модели Pydantic, предназначенные для валидации данных, полученных от сервера, так и для генерации данных.

В директории ui содержатся Page Object и объекты компонентов. Тесты разделены по модулям, такие как users и contacts.  

Еще раздел содержит модуль models, в котором определены модели Pydantic. С их помощью будем как генерировать данные, так и валидировать ответы, полученные от сервера.

В каталоге ui находятся тесты, которые проверяют функциональность приложения с точки зрения конечного пользователя. Эти тесты имитируют реальные пользовательские сценарии, взаимодействуя с пользовательским интерфейсом. Как и в случае с API-тестами, каталог разделен на логические модули.

В корневой папке находится конфигурационный файл config.py, где хранятся настройки проекта, включая пути к тестовому окружению и другие параметры, необходимые для выполнения тестов. Описанная структура проекта позволяет быстро находить нужные тесты и добавлять новые.

├── api/  # API тесты контактов
├── common/
│   ├── api_client.py        # Клиент для взаимодействия с API
│   ├── db_client.py         # Клиент для взаимодействия с базой данных
│   ├── data_factory.py      # Фабрика данных для генерации тестовых данных
│   ├── utils.py             # Вспомогательные функции.
│   └── models.py            # Модели данных для валидации / генерации
│   └── routes.py            # Маршруты для API / UI
├── resources  # Константы / скрипты / sql запросы для тестов
├── framework/
│   ├── ui/
│   │   ├── driver.py  # Драйвер playwright
│   │   ├── element.py  # Базовый элемент для работы со страницей
│   │   ├── list_elements.py  # Списковые элементы для работы со страницей
│   │   ├── fixtures.py  # Базовые фикстуры
├── ui/
│   ├── pages/
│   │   ├── components/
│   │    │   ├── user_dialog.py
│   │    │   ├── contact_dialog.py
│   │   ├── contact_page.py
│   │   ├── user_page.py
│   ├── fixtures.py
│   ├── tests/
│   │   ├── test_contacts.py  # UI тесты для раздела статей
│   │   └── test_users.py     # UI тесты для раздела пользователей
├── .env # Переменные окружения
├── .env.example
├── .gitignore # Исключения для Git
├── .env # Переменные окружения
├── config.py # Конфигурационный файл проекта
├── conftest.py # Корневой conftest
├── docker-compose.yml 
├── Dockerfile 
├── .gitlab-ci.yml # Конфигурация для CI/CD на GitLab
├── poetry.lock  # Файл блокировки зависимостей Poetry
├── pyproject.toml  # Основной файл конфигурации проекта для Poetry
├── pytest.ini # Конфигурационный файл pytest
└── README.md

Driver и BaseElement

Как уже было упомянуто ранее, классы BaseElement и Driver являются ключевыми компонентами при написании UI-тестов. Рассмотрим их подробнее.

В отличие от Selenium, Playwright использует объект Page, который инкапсулирует всё взаимодействие с браузером и элементами страницы в одном месте. Чтобы соблюсти принцип separation of concerns (разделения ответственности), было принято решение разделить логику работы с браузером и взаимодействия с элементами.

Таким образом:

  •  Синглтон Driver отвечает за управление браузером: его инициализацию, создание контекстов, переключение между ними, а также создание новых страниц.

  •  Класс BaseElement представляет собой абстракцию над локаторами и инкапсулирует логику работы с элементами на странице.

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

@dataclass
class By:
    LOCATOR = "locator"
    TEXT = "get_by_text"
    LABEL = "get_by_label"
    TITLE = "get_by_title"
    ALT_TEXT = "get_by_alt_text"
    PLACEHOLDER = "get_by_placeholder"
    TEST_ID = "get_by_test_id"


class BaseElement:
    def __init__(
        self,
        search_by: str,
        locator: str,
        parent: Optional[Union["BaseElement", Locator]] = None,
        ignore_parent: bool = False,
    ):
        self.search_by = search_by
        self.search_locator = locator
        self.parent = parent
        self.ignore_parent = ignore_parent

    def __call__(self, *args, **kwargs):
        if kwargs.get("parent"):
            self.parent = kwargs["parent"]
        self._locator_kwargs = kwargs
        return self

    def _get_locator(self) -> Locator:
        from framework.ui.driver import Driver

        locator: Locator

        if hasattr(self, "_locator_kwargs"):
            formatted_locator = self.search_locator.format(**self._locator_kwargs)
        elif hasattr(self, "search_locator"):
            formatted_locator = self.search_locator
        else:
            formatted_locator = ""

        driver = (
            (
                self.parent._get_locator()
                if isinstance(self.parent, BaseElement)
                else self.parent
            )
            if (self.parent and not self.ignore_parent)
            else Driver().get_driver()
        )
        resolve_method = getattr(driver, self.search_by)
        locator = resolve_method(formatted_locator, exact=self.exact_text)
        return locator
   
    def chain(self, element: "BaseElement"):
        element.parent = self
        return element
   
    def hover(self, **kwargs):
        self._get_locator().hover(**kwargs)

    def should_be_visible(self, should_visible: bool = True) -> None:
        locator = self._get_locator()
        expect(locator).to_be_visible(visible=should_visible)

    def click(self, timeout: int = 2000, force: bool = False):
        self._get_locator().click(timeout=timeout, force=force)

Работа с элементами напоминает работу с элементами в Selenium с некоторыми отличительными особенностями.

В качестве параметров для конструктора элемента передаются тип локатора, и локатор. Минимально работающие варианты выглядят так:

BaseElement(By.LOCATOR, "input[name='login']")
BaseElement(By.TEXT, "Регистрация")
BaseElement(By.TEST_ID, "TopUserMenu")

Процесс получения Playwright-элемента происходит в методе getlocator. В нем локатор принимает финальный вид, форматируется с учетом переданных аргументов, и, если у элемента есть родительский элемент, то сначала резолвится локатор родительского элемента.

Также элементы можно соединять:

login_dialog = BaseElement(By.LOCATOR, ".Mui-popover")
login_dialog.chain(BaseElement(By.TEXT, "Войти")).click()

В этом случае выполнится клик по элементу с текстом «Войти», родительский элемент которого в DOM-дереве должен являться элементом с атрибутом класса “Mui-popover’.

class Driver:
   browser: Browser = None

   @classmethod
   def init_browser(cls, browser: BrowserType):
       cls.browser = browser.launch(
           channel="chrome",
           headless=base_settings.headless_mode,
           slow_mo=300 if base_settings.demo_test else None,
           args=("--start-maximized", '--lang=ru-RU')
       )

    @classmethod
    def new_workspace(cls) -> None:
        if cls.browser:
        new_context = cls.browser.new_context(
            ignore_https_errors=True,
            locale="ru-RU",
           viewport={"width": 1920, "height": 1200},
           timezone_id="Europe/Moscow",
           storage_state=(
               cls.auth_state_path)
           ),
       new_context.set_default_timeout(10000)
       if cls.contexts:
           # 1-9
           new_context_name = (
               f"context_{int(tuple(cls.contexts.keys())[-1][-1])+1}"
           )
       else:
           new_context_name = "context_1"
       page = new_context.new_page()
       cls.contexts[new_context_name] = {
           "context": new_context,
           "pages": [page],
           "selected_page": 1,
       }
   else:
       raise Exception("Browser not initialized. Call init_browser first.")

   @classmethod
   def get_driver(cls) -> Page:
       if cls.current_context is None:
           raise ValueError(
           "Driver is not initialized, call Driver.new_workspace first."
       )
       context = cls._get_current_context_payload()
       return context["pages"][context["selected_page"] - 1]

Для понимания этих классов необходимо знать следующее: в Playwright в отличии от Selenium используется объект page, который объединяет всё взаимодействие с браузером и элементами в одной объекте. Для следования принципу separation of concern было решено чётко разделить взаимодействие с браузером и конкретными элементами. Таким образом, объект Driver отвечает за управление браузером (инициализация браузера, создание контекстов, переключение между ними, создание page), а информация о локаторе абстрагирована в объекте BaseElement. В итоге, работа с локаторами отделена от самого драйвера:

@pytest.fixture(scope="session")
def start_session():
   with sync_playwright() as pw:
       Driver.init_browser(pw.chromium)
       yield
       Driver.close_browser()


@pytest.fixture(autouse=True)
def launch_workspace(start_session):
   Driver.new_workspace()
   yield
   Driver().close_contexts()

Фикстура start_session инициализирует браузер единожды на всю тестовую сессию с помощью параметра scope=”session”. В фикстуре launch_workspace создается новое «рабочее пространство» перед началом каждого теста. После завершения теста (выхода из yield), все созданные контексты закрываются методом Driver().close_contexts().

Page object

def url(_url: str = None):
   def inner(page):
       page.url = f"https://{base_settings.host}/{_url}"
       return page

   return inner

@url()
class UserPage(BasePage):
    login = Element(By.LOCATOR, "input[name='login']")
    add_user_btn = Element(By.LOCATOR, "Добавить")
    create_user_btn = Element(By.LOCATOR, "Создать")
    login_btn = Element(By.TEXT, "Войти")
    password = Element(By.LOCATOR, "input[name='password']")
    grid = Grid()
    alert = Alert()

    def login(self, login: str, password: str):
        self.login.fill(login)
        self.password.fill(password)

    def add_user(self, name: str = Faker.name(), password: str = Faker.password()):
        self.add_user_btn.click()
        self.login.fill(name)
        self.password.fill(password)
        self.create_user_btn.click()

Написание тестов в проекте организовано с помощью паттерна Page object. Объект страницы имеет декоратор url, принимающий часть адресной строки, которая указывает на ресурс, соответствующий этой странице. Далее с помощью метода open можно открыть страницу. В рамках page object инициализируются локаторы, компоненты, и методы работы со страницей. Компоненты представляют собой различные всплывающие диалоговые окна (поп-апы, дроверы и т.д), логика работы с которыми описана в отдельном от page объекте, и которые инициализируются на уровне с обычными локаторами. Но к компонентам мы вернемся позже.

Теперь необходимо подготовить страницу для ее использования в тестах. Напишем фикстуру получения объекта страницы:

@pytest.fixture
def user_page() -> UserPage:
   return Tasks()

UI-тесты

Сейчас предлагаю перейти к написанию первого теста с использованием ранее подготовленных фикстур. Для генерации тестовых данных используем Faker, провайдеры являются стандартными.

def test_login(user_page, api_client):
   name, password = Faker.name(), Faker.password()
   api_client.create_user({"login": name, "password": password})
   user_page.open()
   user_page.login(name, password)
   user_page.grid(contains_text=name).should_be_visible()

В этом тесте мы с помощью Faker генерируем имя и пароль нового пользователя, а затем через api_client создаём соответствующую сущность в приложении. После открытия страницы и выполнения авторизации, проверяем, что пользователь успешно вошёл в систему и отображается в списке на странице.

Напишем еще один тест на создание юзера через ui:

def test_login(request, user_page, api_client):
   name, password = Faker.name(), Faker.password()
   
   def cleanup():
       api_client.delete_user(name)
   request.addfinalizer(cleanup)

   name, password = Faker.name(), Faker.password()
  
   user_page.open()
   user_page.login(name, password)
   user_page.add_user(name, password)
   user_page.grid(contains_text=name).should_be_visible()

В данном тесте используетсяфикстура request. В pytest request — это встроенный объект‑фикстура, который позволяет взаимодействовать с текущим тестом и его окружением. Метод request.addfinalizer(function) регистрирует функцию, которая будет выполнена после окончания теста, вне зависимости от того, на каком этапе он завершится: пройдет успешно или упадёт из-за ошибки.

Напишем еще один тест на проверку негативного сценария:

def test_create_user_with_invalid_password(user_page):
    # Предположим, что наше приложение не принимает пароли короче 6 символов
    name = Faker.name()
    invalid_password = "123"

    user_page.open()
    user_page.add_user(name, invalid_password)
    
    # Проверяем, что система выдала сообщение об ошибке
    user_page.should_have_error_message("Некорректный пароль")
    # Также удостоверимся, что пользователь не появляется в списке
    user_page.grid(contains_text=name).should_not_be_visible()

API-тесты

С UI-частью мы закончили, теперь перейдем к более интересной и необычной — тестированию API. Да, с Playwright возможно и такое. С помощью предоставленного в Playwright функционала можно полноценно тестировать серверную часть, используя его вместо библиотеки requests для отправки запросов. Также удобно использовать API‑запросы для выполнения предусловий в UI‑тестах, например, создание необходимых сущностей через API‑запрос и их дальнейшее использование в пользовательском сценарии UI‑теста.

Это реализуется с помощью объекта APIRequestContext и его методов. Объект APIRequestContext довольно мощный инструмент, который помимо отправки запросов позволяет перехватывать сетевые запросы, что особенно полезно для тестирования таких сценариев, как ошибки сервера или задержки сети. Кроме того, APIRequestContext позволяет создавать моки, то есть подменять ответы сервера на заранее заданные данные. Это особенно полезно для изоляции тестов от внешних зависимостей и обеспечения стабильности тестирования. Приступим к написанию тестов.

Для начала создадим базовую фикстуру для отправки запросов:

@pytest.fixture(scope="session")
def request_context(playwright: Playwright) -> Generator[APIRequestContext, None, None]:
    context = playwright.request.new_context(base_url=base_settings.backend_url)
    yield context
    context.dispose()

Теперь с помощью контекста сделаем фикстуру с авторизацией:

@pytest.fixture(scope="session")
def api_context(request_context: APIRequestContext) -> Generator[APIRequestContext, None, None]:
    payload = AuthPayload(email="your_email", password="your_password").dict()
    res = request_context.post(url=APIRoutes.AUTH, data=payload)
    assert res.ok, res.json()
    res_data = res.json()
    assert "token" in res_data["user"]

    auth_context = request_context.new_context(
        extra_http_headers={"Authorization": f"Bearer {res_data['user']['token']}"},
    )
    yield auth_context
    auth_context.dispose()

 Готово, теперь все запросы с использованием этой фикстуры будут авторизованы.

Напишем первый тест – на создание статьи. Сперва опишем Pydantic-модель:

class Author(BaseModel):
	username: str
	bio: str | None = None
	image: str | None = None
	following: bool
	followers_count: int = Field(..., alias="followersCount")
 
class Article(BaseModel):
	slug: str | None = None
	title: str = Field(default_factory=lambda: fake.sentence(nb_words=10))
	description: str = Field(default_factory=lambda: fake.sentence(nb_words=10))
	body: str = Field(default_factory=lambda: fake.paragraph(nb_sentences=5))
	tag: list[str] = Field(default_factory=lambda: [fake.tags()], alias="tagList")
	updated_at: datetime | None = Field(default=None, alias="updatedAt")
	created_at: datetime | None = Field(default=None, alias="createdAt")
	author: Author | None = None
	favorited: bool | None = None
	favorites_count: int | None = Field(default=None, alias="favoritesCount")
 
class ArticlePayload(BaseModel):
	article: Article

Здесь все уже знакомо, определяем модели Author и Article. Из нового используется параметр alias для объекта Field, указанное в этом параметре значение будет использоваться для сериализации и десериализации данных. Например, followersCount в JSON будет преобразован в followers_count в нашей модели. Также используем ArticlePayload для создания тела запроса, которую ожидает сервер.

Перейдем к написанию первого API-теста:

def test_create_article(api_context, slug):
	article = Article()
	data = ArticlePayload(article=article).model_dump(
    	by_alias=True,
    	exclude={
        	"article": {
            	"author",
            	"slug",
            	"updated",
            	"created_at",
            	"favorited",
            	"favorites_count",
        	}
    	},
	)
	res = api_context.post(APIRoutes.ARTICLE, data=data)
	assert res.ok
	res = res.json()
	slug.append(res["article"]["slug"])
	try:
    	ArticlePayload(**res)
	except ValidationError as e:
    	pytest.fail(f"Validation failed: {e}")

Используем ранее написанную фикстуру, создаем словарь data с помощью метода model_dump, при этом исключаем поля, которые не используется в запросе с помощью параметра exclude. Также в тест передана еще одна фикстура slug, с ее помощью сохраним идентификатор созданной статьи, чтобы использовать ее сущность в дальнейших тестах. Выглядит она незамысловато:

@pytest.fixture(scope="session")
def ids():
	id = []
	yield ids

Теперь напишем тест на добавление комментария к этой статье. Для начала опишем pydantic-модель для комментария:

class Comment(BaseModel):
	id: int
	body: str = Field(default_factory=lambda: fake.sentence(nb_words=10))
	updated_at: datetime | None = Field(default=None, alias="updatedAt")
	created_at: datetime | None = Field(default=None, alias="createdAt")
	author: Author
 
class CommentPayload(BaseModel):
	comment: Comment

Теперь тест:

@allure.title("API: Добавление комментария к статье")
def test_add_comment_to_article(api_context, ids):
	data = CommentPayload().model_dump(
    	by_alias=True, exclude={"comment": {"author", "updated_at", "created_at"}}
	)
	res = api_context.post(f"{APIRoutes.ARTICLE}/{ids[0]}")
	assert res.ok
	try:
    	CommentPayload(**res)
	except ValidationError as e:
    	pytest.fail(f"Validation failed: {e}")

После проведения тестирования важно не забывать о необходимости удаления тестовых данных, перейдем к удалению статьи по API, заодно покроем необходимый функционал:

def test_del_article(api_context, ids):
	res = api_context.delete(f"{APIRoutes.ARTICLE}/{ids[0]}")
	assert res.ok
	res = res.json()
	assert "Article deleted successfully" in res["message"]["body"][0]

Основная структура проекта готова. Мы написали несколько UI- и API-тестов, можно продолжать написание тестов по аналогии, далее перейдем к настройке инфраструктуры.

В качестве инструмента для CI/CD будем использовать GitLab. Перейдем к его настройке и создадим новый проект:

Рис 4. Создание репозитория
Рис 4. Создание репозитория

Теперь нужно задать основные параметры:

Рис 5. Настройки репозитория
Рис 5. Настройки репозитория

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

git init --initial-branch=main
git remote add origin https://gitlab.com/<username>/real-world-app-testing.git git add .
git commit -m "Initial commit" 
git push --set-upstream origin main

Проект выгружен и готов к настройке CI/CD.

Рис 6. Репозиторий
Рис 6. Репозиторий

Для запусков тестов в рамках CI/CD нам понадобится GitLab Runner. Этот компонент является ключевым в системе непрерывной интеграции и доставки в экосистеме GitLab. Он предназначен для выполнения заданий и скриптов, которые определены в .gitlab-ci.yml файле проекта. В нашем случае после каждого коммита раннер будет автоматически собирать проект, запускать тесты и создавать отчет о пройденных тестах.
Для демонстрации я проведу настройку раннера на локальной машине с помощью докера. Настройка раннера на сервере будет аналогичной. Возьмем команду для установки раннера из документации GitLab.

docker run -d --name gitlab-runner --restart always \   -v /var/run/docker.sock:/var/run/docker.sock \   -v /srv/gitlab-runner/config:/etc/gitlab-runner \   gitlab/gitlab-runner:latest

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

Сначала выполним эту команду для запуска процесса регистрации:

  docker exec -it gitlab-runner gitlab-runner register

Раннер предложит пройти процесс регистрации, для этого необходимо указать токен, который находится в проекте по пути Settings → CI/CD вкладка Runners.

Рис. 7. Раннеры проекта
Рис. 7. Раннеры проекта

Также на этой же странице необходимо отключить общедоступные раннеры, предоставленные GitLab, так как теперь у нас есть свой.

Рис 8. Раннеры, предоставляемые GitLab
Рис 8. Раннеры, предоставляемые GitLab

Продолжаем процесс регистрации раннера: все поля можно оставлять со стандартными значениями, указываем токен и последнее, что нужно будет выбрать, executor. В рамках GitLab-runner’a – это компонент, который определяет среду, где будут выполняться задачи (Jobs) нашего раннера. Выбираем docker, теперь у нас есть подготовленная среда для сборки и запуска контейнеров из раннера. Но есть еще один важный момент: чтобы джобы могли взаимодействовать с Docker на нашем хосте, нужно дать доступ к демону докера. Для этого в файле с настройками раннера по пути /etc/giltab‑runner/config.toml (путь в контейнере) добавляем в массив volumes значение «/var/run/docker.sock:/var/run/docker.sock». Таким образом мы подключили сокет Docker»a с хоста к контейнеру задачи.

Готово! Теперь можем обновить страницу в GitLab и увидим наш новенький раннер, ура! :) Для того чтобы раннер начал выполнять задачи, создадим файл .gitlab-ci.yml в корне проекта. Этот файл является основным конфигурационным файлом для системы CI/CD. Он содержит инструкции и определения пайплайнов, которые автоматизируют процессы непрерывной интеграции проекта после каждого коммита или в соответствии с заданными правилами. Напишем конфигурацию:

stages:
  - build
  - run
 
build test:
  image: docker:latest
  stage: build
  script:
	- docker build -f Dockerfile.prod -t test_real_world_app:latest .
  rules:
	- if: $CI_COMMIT_BRANCH == "main"
  	when: always
 
run test:
  image: docker:latest
  stage: run
  before_script:
	- docker rm $(docker ps -aq) || true
	- docker run -d --rm --env-file .env --name test_real_world_app -p 8080:80 test_real_world_app:latest
  script:
	- docker exec test_real_world_app pytest --color=yes
  rules:
	- if: $CI_COMMIT_BRANCH == "main"
  	when: always

Файл состоит из двух основных этапов: сборка ‘build’ и выполнение тестов ‘run’. В первом этапе мы используем команду build для сборки образа из Dockerfile.prod, при этом образ маркируется тегом ‘latest’. Во втором этапе подготавливаем «чистое» тестовое окружении из готового образа и запускаем Docker-контейнер, далее выполняем тесты. Задания CI/CD будут выполняться только в том случае, если изменения внесены в ветку main.

Теперь можно перейти к самой интересной части — к запуску тестов в GitLab. Для этого делаем коммит и пушим в репозиторий, в разделе Build → Pipelines видим похожую картину:

Рис 9. Запущенный пайплайн
Рис 9. Запущенный пайплайн

Пайплайн запущен. Дожидаемся сборки тестового фреймворка, а затем и запуска самих тестов.

Рис 10. Логи пайплайна
Рис 10. Логи пайплайна

В UI GitLab перейдем в последний стейдж. Видим, что тесты успешно пройдены. Однако работа на этом не заканчивается. Зеленые надписи PASSED, конечно, хорошо, но этого явно недостаточно. Следующий шаг — настройка отчетов с помощью Allure. Этот инструмент не только облегчит восприятие результатов, но и повысит их информативность. Allure‑отчет предоставит всесторонний анализ результатов тестов: от отображения истории тестов до подробного описания шагов воспроизведения ошибок. Используя их, команда может быстрее выявлять проблемные области в работе ПО и своевременно устранять их. Таким образом, подключение Allure к нашему CI‑пайплайну позволит нам автоматически генерировать отчеты после каждого выполнения тестов, делая процесс анализа еще более эффективным.

Будем работать с уже знакомым файлом.gitlab‑ci.yml. Для генерации отчета нужно добавить новый этап report в наш пайплайн, который будет выполняться после прогона тестов. Также необходимо обновить команду pytest, добавив флаг alluredir, который указывает каталог, где Allure должен сохранять результаты тестов.

Прилагаю код со стейджами, которые подверглись изменениям:

run test:
  image: docker:latest
  stage: run
  before_script:
	- docker rm $(docker ps -aq) || true
	- docker run -d --rm --memory="500m" --env-file .env --name test_real_world_app -p 8080:80 test_real_world_app:latest
  script:
	- docker exec test_real_world_app pytest --color=yes --alluredir=/allure-results
  after_script:
	- docker cp test_real_world_app:/allure-results ./allure-results
  rules:
	- if: $CI_COMMIT_BRANCH == "main"
  	when: always
 
generate report:
  image: docker:latest
  stage: report
  script:
	- docker run --rm -v $(pwd)/allure-results:/allure-results -v $(pwd)/allure-report:/allure-report frankescobar/allure-docker-service allure generate /allure-results -o /allure-report --clean
  artifacts:
	paths:
  	- allure-report
  rules:
	- if: $CI_COMMIT_BRANCH == "main"
  	when: always

Заключение

В этой статье мы рассмотрели полный процесс автоматизации проекта с нуля, включая использование таких технологий и инструментов, как Playwright, Pytest и Allure, а также настройку CI/CD с помощью GitLab. Ознакомились с тем, как в условиях отсутствия технической документации можно наладить процесс автоматизации тестов, используя знания ручного тестировщика о проекте для получения необходимого набора тест‑кейсов и их последующей автоматизации.

Также мы подробно рассмотрели подготовку инфраструктуры для прогона тестов в GitLab CI с подготовкой отчетов Allure. Мы подготовили фикстуры, необходимые для запуска тестов, и реализовали тестовые сценарии с использованием Playwright в части UI и API. Помимо этого мы рассмотрели оптимальную структуру проекта, позволяющую новому автоматизатору быстро освоиться в проекте и начать пополнять фреймворк новыми тестами.

Далее разработали запуск прогона тестов через UI GitLab, который предоставляет как QA‑специалисту, так и любому заинтересованному лицу прогнать тесты и получить информацию о пройденных тестах в виде Allure‑отчетов.

Несмотря на то, что автоматизация тестирования — не простая задача и требует значительных усилий, мы на практике проверили, что она вполне осуществима и приносит ощутимые результаты. После ее внедрения время на выполнение регрессионного тестирования сократилось с 5 часов до 40 минут, а количество обнаруживаемых багов на ранних стадиях увеличилось на 40%. Это позволило внедрить новые изменения с минимальными рисками и улучшить качество выпускаемого продукта.

Спасибо за внимание!

Больше авторских материалов для SDET‑специалистов от моих коллег читайте в соцсетях SimbirSoft — ВКонтакте и Telegram.

Теги:
Хабы:
+6
Комментарии0

Публикации

Информация

Сайт
www.simbirsoft.com
Дата регистрации
Дата основания
Численность
1 001–5 000 человек
Местоположение
Россия