Привет, Хабр! Меня зовут Евгения Мясникова, я QA-инженер SpaceWeb. Передавать искусственному интеллекту рутинные задачи тестирования — подготовку черновиков тест-кейсов, первичное покрытие, оценку трудозатрат и другие — уже стало привычной практикой. Однако при таком разнообразии инструментов важно понять, как эффективно встроить ИИ-агентов в рабочие процессы и распределить задачи между специалистом и нейросетью.

В этой статье расскажу, как мы совместно с руководителем отдела исследований и разработки SpaceWeb Виталием Киреевым разработали первоначальную модель взаимодействия QA-специалиста с LLM. Для работы с чувствительными данными мы интегрировали агента с локальной моделью Qwen3.8-27B. На реальных примерах покажу, в каких задачах ИИ действительно нам помог, а где его применение пока вызывает вопросы.

Навигация по тексту:

  1. Какие задачи тестирования мы решили передать ИИ

  2. Как организовали работу QA-инженеров с LLM

  3. Практические кейсы: тестовая документация, автотесты и проверка API

  4. Как ИИ повлиял на скорость и результаты тестирования

Какие задачи тестирования мы решили передать ИИ

Тестировщики в современной ИТ-компании работают с огромной кодовой базой, десятками API-методов и постоянным потоком новых фич. Запросы от клиентов не заканчиваются, разработка идет полным ходом, а QA-инженерам приходится искать компромисс между качеством тестирования и скоростью релизов.

Мы решили выяснить, в каких задачах LLM действительно экономит время, а где генерирует красивую воду, которую потом всё равно приходится переписывать вручную.

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

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

Как организовали работу QA-инженеров с LLM

После анализа задач и выявления узких мест мы начали внедрять ИИ в процесс тестирования. Автоматизировать всё сразу невозможно, поэтому для начала выбрали три направления:

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

  • написание скриптов для автоматизации. LLM генерирует код по промпту, а QA-инженер отлаживает его и интегрирует в проект. Готовые скрипты можно повторно использовать для других подходящих задач;

  • расширение сценариев тестирования безопасности и API. ИИ помогает генерировать дополнительные проверки и тестовые комбинации, анализировать исходный код и JSON-схемы.

Тестовая документация, автотесты и проверка API

Для генерации тестовой документации, кода и скриптов мы использовали внутреннего LLM-ассистента. Непосредственно для тестирования API и запуска UI-автотестов применяли Postman, Python, Pytest и Selenium.

Кейс 1. Генерируем тест-кейсы и данные для проверки Serverless и VPS

Задача: проверить форму создания приложения облачного сервиса Serverless.

Загружаем предусловия, требования к обязательности полей и валидации, а также основные сценарии работы с формой. Просим ИИ составить список позитивных проверок и оформить его в виде таблицы.

Получаем базовый список проверок по позитивному сценарию:

Затем просим ИИ дополнить список негативными сценариями:

Отдельного внимания заслуживает интеграция с GitHub/GitLab. Просим ИИ-ассистента сгенерировать проверки для этого функционала и выделить наиболее критичные. Получаем довольно хороший список:

Создаем тестовые данные для проверки API

По заданному промпту агент генерирует необходимое количество синтетических, но реалистичных данных: банковские счета, имена, логины, пароли, домены и т. д.

Мы попробовали использовать его для создания JSON-объектов при тестировании создания VPS.

Передаем агенту пример валидного JSON и просим подготовить данные для разных проверок:

  • корректные запросы — минимальные и эталонные;

  • граничные значения для проверки лимитов;

  • поддержку кириллических доменов и специальных символов.

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

{
  "hostname": "a-really-long-hostname-that-is-exactly-sixty-three-characters-long-to-test-dns-limits-12345",
  "region": "us-west",
  "plan": "baremetal-ultra",
  "image": "windows-server-2022",
  "ssh_keys": [],
  "user_data": "%0A%0A%0A%0A" (пустые строки, закодированные для проверки обрезки),
  "tags": [
    "x" * 50,
    "y" * 50,
    "z" * 50
  ],
  "ipv6": "2001:0000 0000:0000:0000:0000:0000:7334"
}

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

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

{
  "hostname": "geo-test",
  "region": "atlantis",
  "plan": "cloud-1",
  "image": "ubuntu-22.04"
}

Кейс 2. Создаем UI-автотест с помощью LLM меньше чем за час

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

Задача: автоматизировать создание почтового ящика в панели управления виртуальным хостингом и проверить позитивный сценарий.

Промпт:

Напиши код на Python с использованием библиотек Pytest, Selenium и драйвером Сhrome и учетом того, что авторизация подхватывается из файла conftest.py, который выполнит следующие действия по шагам. 

  1. Перейти на страницу добавления почтового ящика.

  2. Найти input с атрибутом name="mbox" и заполнить его именем ящика. Допустимые символы: строчные латинские буквы, цифры, +, -, ., _. Максимальная длина — 64 символа.

  3. Найти элемент span с классом password-generator__text и кликнуть на него.

  4. Проверить, что после этого в input с id="passwordWithGenerator" появился пароль длиной от 8 до 20 символов, содержащий буквы, цифры и специальные символы -!_@#$%^&*.

  5. Найти кнопку button с текстом «Создать» и атрибутом type="submit", затем кликнуть на нее.

После выполнения этих шагов необходимо проверить, что произошел редирект на страницу /mail_add/review, а созданный почтовый ящик появился в списке.

Нейросеть сгенерировала готовый скрипт test_mail_add.py с подробными комментариями и необходимыми проверками по заданному промпту.

import os
import string
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC


# Замените на реальный URL тестового стенда через переменную окружения или конфиг
BASE_URL = os.environ.get("BASE_URL", "https://YOUR_TEST_ENV_HOST:PORT")


def _generate_mbox_name() -> str:
    """Генерирует случайное имя почтового ящика (12 символов)."""
    allowed = string.ascii_lowercase + string.digits + "+-._"
    length = 12
    return "".join(allowed[b % len(allowed)] for b in os.urandom(length))


class TestMailAdd:
    """Тест создания почтового ящика через панель управления SpaceWeb."""

    def test_create_mail_box(self, login_user):
        driver = login_user
        wait = WebDriverWait(driver, 15)

        # Шаг 1: Переход на страницу добавления почты
        driver.get(f"{BASE_URL}/hosting/mail/mail_add/")

        # Шаг 2: Заполнение поля mbox допустимым именем ящика
        mbox_value = _generate_mbox_name()
        mbox_input = wait.until(
            EC.presence_of_element_located((By.CSS_SELECTOR, 'input[name="mbox"]'))
        )
        mbox_input.clear()
        mbox_input.send_keys(mbox_value)

        # Шаг 3: Клик по генератору пароля
        generator_span = wait.until(
            EC.element_to_be_clickable((By.CSS_SELECTOR, "span.password-generator__text"))
        )
        generator_span.click()

        # Шаг 4: Проверка сгенерированного пароля
        password_input = wait.until(
            EC.presence_of_element_located((By.ID, "passwordWithGenerator"))
        )
        wait.until(lambda d: len(password_input.get_attribute("value") or "") >= 8)
        password = password_input.get_attribute("value")

        assert 8 <= len(password) <= 20, (
            f"Длина пароля {len(password)} вне диапазона 8–20"
        )
        allowed_chars = set(string.ascii_letters + string.digits + "-!_@#$%^&*")
        invalid = [ch for ch in password if ch not in allowed_chars]
        assert not invalid, f"Недопустимые символы в пароле: {invalid}"

        # Шаг 5: Нажатие кнопки "Создать"
        submit_btn = wait.until(
            EC.element_to_be_clickable(
                (By.XPATH, '//button[text()="Создать" and @type="submit"]')
            )
        )
        submit_btn.click()

        # Финальная проверка: редирект на страницу review
        wait.until(EC.url_contains("/hosting/mail/mail_add/review"))
        expected_url = f"{BASE_URL}/hosting/mail/mail_add/review"
        assert driver.current_url == expected_url, (
            f"Ожидалось {expected_url}, фактически {driver.current_url}"
        )

        # Проверка отображения созданного ящика на странице review
        mailbox_displayed = wait.until(
            EC.presence_of_element_located(
                (By.XPATH, f"//*[contains(text(), '{mbox_value}')]")
            )
        )
        assert mailbox_displayed.is_displayed(), (
            f"Ящик {mbox_value} не отображается на странице review"
        )

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

Получаем код с параметризацией test_mail_add_parametrs.py. Позитивный и негативный сценарии разделены на два теста и выполняются в разных методах: test_create_valid_mail_box и test_reject_invalid_mail_box.

import os
import string
import pytest
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC


BASE_URL = os.environ.get("BASE_URL", "https://YOUR_TEST_ENV_HOST:PORT")


def _generate_mbox_name(length=12) -> str:
    """Генерирует случайное имя ящика из разрешённых символов."""
    allowed = string.ascii_lowercase + string.digits + "+-._"
    return "".join(allowed[b % len(allowed)] for b in os.urandom(length))


# Позитивные кейсы: имя должно быть принято
POSITIVE_CASES = [
    ("test.user+mail-2026", "базовое имя со спецсимволами"),
    ("a", "минимальная длина 1 символ"),
    ("a" * 64, "максимальная длина 64 символа"),
    ("user.name_99", "цифры, точка и подчёркивание"),
]

# Негативные кейсы: имя должно быть отклонено
NEGATIVE_CASES = [
    ("UPPER.case", "заглавные буквы не допускаются"),
    ("name with spaces", "пробелы не допускаются"),
    ("a" * 65, "длина больше 64 символов"),
    ("bad/name", "символ '/' не входит в допустимый набор"),
    ("emoji", "не-латинские символы"),
]


class TestMailAdd:

    @pytest.mark.parametrize("mbox_name, description", POSITIVE_CASES,
                             ids=[d for _, d in POSITIVE_CASES])
    def test_create_valid_mail_box(self, login_user, mbox_name, description):
        """Позитивный сценарий: ящик создаётся успешно."""
        driver = login_user
        wait = WebDriverWait(driver, 15)

        # Если имя не уникальное (фиксированные строки), добавляем суффикс
        if not any(c.isdigit() for c in mbox_name[-8:]):
            mbox_name = f"{mbox_name}.{_generate_mbox_name(6)}"

        driver.get(f"{BASE_URL}/hosting/mail/mail_add/")

        mbox_input = wait.until(
            EC.presence_of_element_located((By.CSS_SELECTOR, 'input[name="mbox"]'))
        )
        mbox_input.clear()
        mbox_input.send_keys(mbox_name)

        generator_span = wait.until(
            EC.element_to_be_clickable((By.CSS_SELECTOR, "span.password-generator__text"))
        )
        generator_span.click()

        password_input = wait.until(
            EC.presence_of_element_located((By.ID, "passwordWithGenerator"))
        )
        wait.until(lambda d: len(password_input.get_attribute("value") or "") >= 8)
        password = password_input.get_attribute("value")

        assert 8 <= len(password) <= 20, (
            f"[{description}] Длина пароля {len(password)} вне диапазона 8–20"
        )
        allowed_chars = set(string.ascii_letters + string.digits + "-!_@#$%^&*")
        invalid = [ch for ch in password if ch not in allowed_chars]
        assert not invalid, f"[{description}] Недопустимые символы в пароле: {invalid}"

        submit_btn = wait.until(
            EC.element_to_be_clickable(
                (By.XPATH, '//button[text()="Создать" and @type="submit"]')
            )
        )
        submit_btn.click()

        wait.until(EC.url_contains("/hosting/mail/mail_add/review"))
        expected_url = f"{BASE_URL}/hosting/mail/mail_add/review"
        assert driver.current_url == expected_url, (
            f"[{description}] Ожидалось {expected_url}, фактически {driver.current_url}"
        )

        mailbox_displayed = wait.until(
            EC.presence_of_element_located(
                (By.XPATH, f"//*[contains(text(), '{mbox_name}')]")
            )
        )
        assert mailbox_displayed.is_displayed(), (
            f"[{description}] Ящик {mbox_name} не отображается на странице review"
        )

    @pytest.mark.parametrize("mbox_name, description", NEGATIVE_CASES,
                             ids=[d for _, d in NEGATIVE_CASES])
    def test_reject_invalid_mail_box(self, login_user, mbox_name, description):
        """Негативный сценарий: ящик НЕ создаётся, показывается ошибка."""
        driver = login_user
        wait = WebDriverWait(driver, 15)

        driver.get(f"{BASE_URL}/hosting/mail/mail_add/")

        mbox_input = wait.until(
            EC.presence_of_element_located((By.CSS_SELECTOR, 'input[name="mbox"]'))
        )
        mbox_input.clear()
        mbox_input.send_keys(mbox_name)

        # Вариант А: фронтенд блокирует отправку (кнопка неактивна или появляется инлайн-ошибка)
        error_visible = wait.until(
            EC.presence_of_element_located(
                (By.CSS_SELECTOR, ".error-message, .form-error, [role='alert']")
            )
        )
        assert error_visible.is_displayed(), (
            f"[{description}] Не отображена ошибка для недопустимого имени '{mbox_name}'"
        )

        # Вариант Б: если форма всё же отправляется — проверяем, что редиректа на review нет
        # и URL остался на mail_add/
        # assert "/mail_add/review" not in driver.current_url

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

На написание промпта и поиск нужных селекторов ушло около 30 минут, на отладку и уточнения — еще 20. Так менее чем за час мы получили готовый автотест.

Кейс 3. Проверяем безопасность API и ищем IDOR-уязвимости

При тестировании API-методов мы пробовали использовать ИИ для генерации нескольких типов проверок:

  • контрактные проверки — коды ответов, структура тела ответа, бизнес-логика метода;

  • проверки производительности — например, как метод ведет себя при передаче объемного JSON в теле запроса;

  • проверки безопасности — возможные утечки данных и различные уязвимости.

Задача: проверить IDOR-уязвимость в методе редактирования услуги.

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

JSON-схема метода edit:

{
  "jsonrpc": "2.0",
  "method": "edit",
  "params": {
    "service_ref": "new_balancer_7",
    "scheduling_strategy": "weighted",
    "backend_endpoints": "ip: 10.0.0.11",
    "traffic_profile": "protocolBalancerV2",
    "active_probe": "true",
    "client_ip_passthrough": "true",
    "connection_reuse": "false",
    "sticky_sessions": "true",
    "display_name": "Новый балансировщик 7"
  }
}

Первое, что предлагает агент, — отправить тот же запрос, но с service_ref другого пользователя. Ок, базовая проверка есть. Идем дальше и пробуем автоматизировать перебор значений service_ref.

Просим ассистента написать код на Python с использованием библиотеки Requests. Предварительно создаем TXT-файл со списком реальных service_ref, к которым у текущего пользователя нет доступа.

Получаем следующий код:

import requests

URL = "https://api.example.com/jsonrpc"
TOKEN = "YOUR_BEARER_TOKEN"
FILE = "service_refs.txt"


def load_service_refs(path):
    with open(path, encoding="utf-8") as f:
        text = f.read()

    # Поддерживает значения по строкам или через запятую
    return [
        item.strip()
        for item in text.replace(",", "\n").splitlines()
        if item.strip()
    ]


service_refs = load_service_refs(FILE)

session = requests.Session()
session.headers.update({
    "Authorization": f"Bearer {TOKEN}",
    "Content-Type": "application/json",
})

for service_ref in service_refs:
    payload = {
        "jsonrpc": "2.0",
        "method": "edit",
        "params": {
              "params": {
              "service_ref": "new_balancer_7",
              "scheduling_strategy": "weighted",
              "backend_endpoints": "ip: 10.0.0.11",
              "traffic_profile": "protocolBalancerV2",
              "active_probe": "true",
              "client_ip_passthrough": "true",
              "connection_reuse": "false",
              "sticky_sessions": "true",
              "display_name": "Новый балансировщик 7"
        },
    } }

    response = session.post(URL, json=payload, timeout=10)

    try:
        result = response.json()
    except ValueError:
        result = response.text

    print(service_ref, response.status_code, result)

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

Как внедрение LLM повлияло на количество тестов и ошибок

Мы собрали статистику и оценили эффективность применения ИИ по ключевым направлениям.

  • Скорость подготовки тестовых данных и документации выросла в среднем на 70%. ИИ взял на себя значительную часть рутинной и шаблонной работы.

  • Время подготовки автотестов сократилось на 30%. ИИ оказался полезным помощником в автоматизации, однако весь сгенерированный код по-прежнему требует тщательного ревью специалистом.

  • Количество проверок API-методов на безопасность и валидацию увеличилось на 40%. С помощью ИИ мы расширили набор сценариев за пределы стандартных проверок по документации.

  • Количество багов, с которыми пользователи столкнулись на продакшене, уменьшилось на 70% (по метрике bugs per KLOC).

При этом общее количество тест-кейсов выросло на 35%, а время, которое QA-специалисты затрачивают на тестирование, сократилось на 20% (по метрикам времени, затраченного на фиксированный объем тестов).

По результатам эксперимента мы считаем внедрение ИИ в процессы тестирования успешным. В дальнейшем планируем масштабировать этот подход и расширять применение нейросетей в работе QA-инженеров.

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

А какие задачи тестирования вы уже доверяете ИИ?