Может ли ИИ писать автотесты за QA-инженера? Проверили на реальных задачах. llm.. llm. pytest.. llm. pytest. python.. llm. pytest. python. qa.. llm. pytest. python. qa. qwen.. llm. pytest. python. qa. qwen. selenium.. llm. pytest. python. qa. qwen. selenium. автоматизация тестирования.. llm. pytest. python. qa. qwen. selenium. автоматизация тестирования. Блог компании Рунити.. llm. pytest. python. qa. qwen. selenium. автоматизация тестирования. Блог компании Рунити. генеративный ии.. llm. pytest. python. qa. qwen. selenium. автоматизация тестирования. Блог компании Рунити. генеративный ии. искусственный интеллект.. llm. pytest. python. qa. qwen. selenium. автоматизация тестирования. Блог компании Рунити. генеративный ии. искусственный интеллект. локальные llm.. llm. pytest. python. qa. qwen. selenium. автоматизация тестирования. Блог компании Рунити. генеративный ии. искусственный интеллект. локальные llm. Машинное обучение.. llm. pytest. python. qa. qwen. selenium. автоматизация тестирования. Блог компании Рунити. генеративный ии. искусственный интеллект. локальные llm. Машинное обучение. Тестирование IT-систем.. llm. pytest. python. qa. qwen. selenium. автоматизация тестирования. Блог компании Рунити. генеративный ии. искусственный интеллект. локальные llm. Машинное обучение. Тестирование IT-систем. тестирование по.
Может ли ИИ писать автотесты за QA-инженера? Проверили на реальных задачах - 1

Меня зовут Евгения Мясникова, я 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.

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

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

Может ли ИИ писать автотесты за QA-инженера? Проверили на реальных задачах - 2

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

Может ли ИИ писать автотесты за QA-инженера? Проверили на реальных задачах - 3

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

Может ли ИИ писать автотесты за QA-инженера? Проверили на реальных задачах - 4

Создаем тестовые данные для проверки 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-инженеров.

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

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

Автор: runity

Источник