Всем привет, меня зовут Илья. И я использую LLM в своей работе уже полгода. Хочу поделиться своим опытом и наблюдениями, как ИИ может повысить эффективность работы. И стоит ли бояться тестировщикам замены железными мозгами и руками?

Тестировщики встречаются с ИИ лицом к лицу
Тестировщики встречаются с ИИ лицом к лицу

Будет справедливым сказать, что ИИ, LLM - это отличная база для знакомства с какой-либо областью или предметом, и тестирование - не исключение. ИИ отлично справляется с объяснением теории тестирования, в интерактивном режиме может объяснить лучшие подходы, ссылаясь на первоисточники, книги, ответить на вопросы и помочь разобраться начинающему тестировщику. Раз так, то возникает резонный вопрос - сможет ли ИИ полностью заменить тестировщика? И этот вопрос неоднозначный. Учитывая темпы развития ИИ - ответ положительный, с другой стороны, в какой степени?

Ручное тестирование и база знаний

Я каждый день пользуюсь ИИ помощниками в своей работе, и сейчас возможности ИИ сравнимы с несколькими Junior-подопечными тестировщиками. То есть, сейчас ИИ может очень неплохо справляться с тем, что могут делать молодые специалисты, находясь под контролем опытного руководителя, senior’a. Скажем, в зависимости от ИИ, и того, как вы его используете, ИИ очень неплохо справляется в том числе и с работой ручного тестировщика - написанием чек-листов, тест-дизайнера - подробным описанием тест-кейсов. Но с тем нюансом, что вам, как старшему тестировщику всё же нужно его контролировать и корректировать, а, главное, хорошо разбираться в своей предметной области. Дело в том, что ИИ может неплохо объяснять и выполнять задачу на пальцах, самым понятным и очевидным способом. Это значит не ожидайте от него сложных взаимосвязанных, а главное глубоких сценариев, не ожидайте от него разделения тестовых данных от кейсов (хотя он прекрасно знает, как правильно). То есть каждый раз вам придётся его корректировать - переделай, доделай, углуби. И каждый раз вопрос проработки кейсов - это будет вопрос размера контекста, инструкций, с которым работает ИИ. Это схоже с тем, как обучать под себя юного тестировщика, под ваше видение, под нужную вам глубину и правильность, под ваши возможности и стоимость - по деньгам и времени. А дальше - передавать контекст перед генераций каждый раз, или переобучить модель под ваши специфические нужды. Другим словами, без большого контекста, отдать ему, как мидлу, работу не получится, плюс у него нет механизма контроля - он ляжет на вас. Но о контроле чуть попозже.

Если у нас есть требования, чтобы передать боту или зададим уточняющие вопросы, то можно получать более детальные чек-листы и тесты
Если у нас есть требования, чтобы передать боту или зададим уточняющие вопросы, то можно получать более детальные чек-листы и тесты

При этом, ИИ - это не только джун для делегирования рутины, но так же ваш верный помощник и справочник, очень вовлечённый в решение вашей проблемы. Предоставьте ему лог, ошибку, задачу о том, как настроить какое-либо окружение или инструмент, и ИИ быстро и отлично справится, без необходимости гуглить stackoverflow. Но надо понимать, что большинство моделей актуальны на какую-либо дату, обучения, и их модель не будет знать об изменениях сделанных в последние недели, месяцы, а то и год. Если, конечно, она не имеет доступа к интернет-поиску.

Далее я перейду к автоматизированному тестированию. Итак, для чего лично я использую ИИ и какой?

Документация


Я использую Copilot от GitHub уже третий месяц. То есть, плачу за него $10 в месяц. Стоит ли этого? Честно скажу, что для меня - да. Мне очень нравится делегировать Copilot’у написание комментариев, докстрингов и прочей документации. В большинстве случаев у него это неплохо получается, но если вам нужно нечто другое, то он со второй-третьей попытки подхватывает то, что вы от него хотите - в каком стиле писать докстринги, какими терминами, и из каких (саб)модулей. Одна эта функциональность для меня окупает моё время, которое я бы потратил на написание документации.

Просто пробегаемся по коду и заполняем комментарии - Copilot напишет их исходя из кода
Просто пробегаемся по коду и заполняем комментарии - Copilot напишет их исходя из кода

Написание кода

Но функциональность copilot’a не ограничена лишь генерацией документации. Он может генерировать и код. Для простоты можно считать, что copilot - это усовершенствованная версия подсказок IDE. Во время написания кода он будет выплёвывать вам наиболее вероятные продолжения вашего кода. Это не всегда работает, но, как минимум, у вас будет возможность выбрать как вариант copilot’a, так и классическую подстановку IDE. Что же касается генерации кода как такого из описания, а тем более из названия функции, то тут всё обстоит хуже. ИИ может генерировать скелетны и какие-то банальные функции, либо варьировать те, которые вы уже успели написать. Но если вам нужна какая-то специфичная логика, то, увы, придётся писать её самому. И тут важный нюанс - как инструмент разработчика copilot’ скорее мешает, чем помогает - он легко может генерировать неверный код, который вам потом придётся отлаживать. Но вот как инструмент автоматизатора тестирования он очень даже неплох - выдать схожие, по своей сути, тесты буквально за нажатие 2-3 клавиш на клавиатуре - это ускорение работы на порядок. Нужно протестировать форму? Напишите 1-2 теста, а потом начинайте писать название теста и вот уже у вас написаны все остальные.

Чем больше вы пишите кода, тем адаптивнее пишет помощник. Начиная с дополнения до генерации целой функции. Либо по аналогии, либо исходя из её названия. Даже без текстового описания.
Чем больше вы пишите кода, тем адаптивнее пишет помощник. Начиная с дополнения до генерации целой функции. Либо по аналогии, либо исходя из её названия. Даже без текстового описания.

Итого, как и в случае с “джуном”-ии, так и в случае с copilot’ом, вам нужно будет показать ему примеры, повести и показать как правильно писать тесты - использовать генераторы вместо хардкода, ну и иметь готовую POM (ну или хотя бы часть её). Да, её можно попытаться получить из генерации, но не всё так радужно, как дополнение её по аналогии.

Copilot умеет генерировать и юнит-тесты и объяснять чужой код. Но и то, и другое - не очень глубоко.
Copilot умеет генерировать и юнит-тесты и объяснять чужой код. Но и то, и другое - не очень глубоко.

С другой стороны, Сopilot может неплохо объяснять чужой код, если вам это нужно. Исправлять ошибки и оптимизировать код (учитите, он может это делать специфично и даже выкидывать то, что вам нужно - но, опять-таки вы можете его скорректировать). А так же генерировать юнит-тесты на основании исходного кода вашего ПО, причём достаточно аккуратно и глубоко, с мокированием. Это достаточно впечатляет, но нужно понимать, что всегда есть вероятность, что что-то не заработает ни с первого, ни с пятого раза, так как опять у Copilot’a, как и любого другого ии будет проблема с величиной контекста. То есть глубиной ваших вызовов, кода, и его сложности и нюансов. То есть, как и в случае с мануальным ИИ-джуном, всё равно нельзя делегировать ему задачу “покрой мою функционально 100% тестами”. Не покроет. По крайней мере без вашего активного участия.

Делегируем написание page object'ов и тестов

Вне зависимости от того, используете вы или нет Copilot, ИИ можно делегировать и другую рутину. Например, каждый день, когда я пишу тесты, я прошу LLM написать мне локаторы или сразу PO-объекты, конечно, соответствующие моему шаблону. То есть я передаю ИИ боту исходный код страницы и прошу его сгенерировать для меня page object'ы. Это ускоряет мою работу, но при этом, всё равно получаемый результат нужно контролировать и итеративно улучшать. Например, локаторы, которые быстро предложит бот, будут далеки от идеальных, и нужно будет уточнять, что и как вы хотите укоротить. Бот - тот же джун, которому нужно объяснить, что не нужно просто брать и копировать из браузера огромный XPATH, который сломается при первом же изменении фронтенда.

Без объяснений и обучений ИИ генерирует "заборы" вместо нормальных локаторов
Без объяснений и обучений ИИ генерирует "заборы" вместо нормальных локаторов

Ну конечно, это касается не только UI тестирования, но и API - никто вам не мешает передать JSON файл Swagger’a и попросить написать тестовые зеркальные endpoint’ы и тесты на ответы и параметры к ним.

В целом, если у вас хороший сервис, вы можете получить готовые тесты прямо налету. При необходимости - передать модели требования, документацию и скорректировать её работу исходя из вашей тестовой модели. То есть указываете модели URL, говорите написать тесты, затем переписать в POM-виде - и вот они готовые тесты. Более того, есть инструменты которые не только позволяют вам написать на лету тесты, но вживую, “потыкать” ваш сайт роботом. Выглядит впечатляюще и заменяет работу ручного тестировщика, у которого вы сидите за спиной. Только еще того, который сразу записывает за собой код автотестов. Ну это я говорю о достаточно кусачем по цене Sider.ai.

Растим электронного Middle

И тут мы подходим к самой актуальной проблеме - отсутствие у ИИ вашего контекста. Скажем, какой бы замечательный не был бы ИИ, какой-то тестовой куки \ логина через SSO для получения информации закрытых вовне страниц у него не будет. Если вам нужен свой стиль и код - ему нужно предоставить свою кодовую базу или доки, скажем, которые вы упакуете в embeddings. Это первая проблема - наверняка ваши автотест уже используют какой-то ваш тестовый фреймворк и обвязки, готовые функции, о которых ИИ не знает. Во-вторых, было бы неплохо не сидеть с ИИ над душой и копировать HTML/JSON ему, затем получать ответ, копировать в ваш код, и затем самому проверять работоспособность. Вот возникнут вопросы - пусть приходит с результатом. Ну чтобы не джун был, а такой вот электронный мидл. Возможно ли?

Вместо джуна вы будете сидеть и кричать на ИИ. И да, он будет тупить и бесить не хуже человеков.
Вместо джуна вы будете сидеть и кричать на ИИ. И да, он будет тупить и бесить не хуже человеков.

Задача вырастить джуна на ручном приводе в полуавтоматического мидла ляжет на вас. Но это вполне выполнимо. Вообще говоря, у ИИ все необходимые детали есть чтобы собраться в мидла. Локаторы, page object’ы он умеет генерировать, тесты на их основе - тоже. Нужно объединить, и дать ему возможность получать обратную связь о той ерунде, что он написал. То есть обратно ему отдавать ошибки, чтобы он исправлял код. Хотите починить стили - отдавайте ошибки линтеров. Пусть исправляет до тех пор, пока не получит 100% пассрейт.

В качестве прототипа, proof-of-concept, я буду использовать свою обёртку для OpenAI. Для того, чтобы получить что-то вразумительно от ИИ, я буду использовать ChatGPT4, а так же мне нужно будет использовать функции, которые будут делать всю чёрную работу - звать получать контент страниц и звать тесты. То есть нам нужна будет модель gpt-4-0613. В качестве фреймворка я буду использовать pytest+selenium для простоты (ну и потому что обёртка тоже на Python).

Принципиальная схема прототипа
Принципиальная схема прототипа

Весь мой POC тест-фреймворк будет состоять из одного лишь conftest c фикстурой driver, пускалки (runner’a) и по умолчанию будем считать, что на каждую уникальную страницу у нас де-факто имеется фикстура, которая будет открывать нужную страницу для тестирования. В реальной жизни, вероятно, перед стартом теста мы ещё бы проходили аутентификацию и как-то вертели окружение. Но это опустим в контексте прототипа. Мы уже, фактически, запускаем тесты из реального окружения.

conftest.py
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service as ChromeService
from webdriver_manager.chrome import ChromeDriverManager


def pytest_runtest_makereport(item, call):
    """
    Pytest hook for saving html page on test failure

    :param item: pytest item
    :param call: pytest call
    """
    if "driver" in item.fixturenames:
        web_driver = item.funcargs["driver"]
        if call.when == "call" and call.excinfo is not None:
            with open(f"{item.nodeid.split('::')[1]}.html", "w", encoding="utf-8") as file:
                file.write(web_driver.page_source)


@pytest.fixture
def driver():
    """
    Pytest fixture for selenium webdriver

    :return: webdriver
    """
    options = Options()
    options.add_argument("--headless")
    options.headless = True
    path = ChromeDriverManager().install()
    _driver = webdriver.Chrome(service=ChromeService(executable_path=path, options=options), options=options)

    yield _driver

    _driver.close()
    _driver.quit()

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

  1. Попросить получать код страницы (вызывая определённую функцию), и затем для него сгенерировать json, который будет содержать page object’ы и тесты в определённом формате.

  2. Запускать определённый тест и получить результат. Если есть ошибка, то исправить её.

  3. Повторить 2. 

При необходимости дополнять тесты пользовательским контекстом.

Системные инструкции для бота
1) You may obtain page code by calling "get_page_code" function. It will return you:
 raw HTML document, what needs to be tested. And you need to respond with json in following format:
{
"page_objects": [
"@property\\n
    def calculate_button(self):\\n
        return WebDriverWait(self.driver, 10).until(\\n
            EC.presence_of_element_located((By.XPATH, '//button[.='''Calculate''']'))\\n
        )", <...>
],
"tests": ["def test_division_by_zero(page):\\n
    page.numbers_input.send_keys(1024)\\n
    page.divide_button.click()\\n
    page.calculator_input.send_keys('0')\\n
    page.calculate_button.click()\\n
    assert page.error.text() == 'Error: divide by zero'", <...>],
}
This means you need to create page objects for each object on the page using laconic and stable XPATH locators (as short and stables as you can, use only By.XPATH locators, not By.ID, not By.CSS_SELECTOR or By.CLASS name), and then create all possible test cases for them. It might be some filed filling tests (errors, border checks, positive and negative cases), clicking, content changing, etc. Please respect to use 'page' fixture for every test, it's predefined in code and opens page under test before it.
2) Then I may ask you to execute some tests. You can run demanded test via "get_tests_results" function, based on gathered content, you need to respond with json in following format:
results = {
    "passed": [],
    "failed": [],
    "error": [],
    "failure details": {}
}
where "failure details" - is dict with keys equal to test names (which you generated) and possible failures details. If you got an failures and errors, you need to respond as in 1 with fixed code (page objects and/or tests).
Answer only with JSON in format I mentioned in 1. Never add anything more than that (no explanations, no extra text, only json).
3) In addition to 1 and 2 i may pass you extra info what kind of test data might be used (i.e. for form filling), but in general you need to generate all possible scenarios (valid/invalid/border cases, always add what's not listed by user, but should be for best quality of testing coverage).

Для того, чтобы немного поэкономить токены, получаем страницу (лучше это делать не request’ом, а с помощью selenium, чтобы отработал на странице возможный javascript), затем убираем всё лишнее - то есть оставляем только body и убираем все script’ы. Вы вполне можете расширить функциональность так, как вам нужно, например, убирая повторяющиеся элементы (сайдбары, хэдеры и прочая), забирая только какие-то определённые формы, и т.д.. Для этого я написал класс PageRetriever.

page_retriever.py
from bs4 import BeautifulSoup
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service as ChromeService
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from webdriver_manager.chrome import ChromeDriverManager


class PageRetriever:
    """The PageRetriever class is for managing an instance of the PageRetriever."""

    def __init__(self, url=""):
        """
        General init.

        :param url: (str) URL of the page.
        """
        options = Options()
        options.add_argument("--headless")
        options.headless = True
        path = ChromeDriverManager().install()
        self.driver = webdriver.Chrome(service=ChromeService(executable_path=path), options=options)
        self.url = url

    def set_url(self, url):
        """
        Set the url.

        :param url: (str) URL of the page.
        """
        self.url = url

    def get_page(self, url=None):
        """
        Get the page content from the url.

        :param url: (str) URL of the page.
        :return: (str) HTML content of the page.
        """
        if url:
            self.set_url(url)
        return self.get_page_content(self.url)

    def get_body(self, url=None):
        """
        Get the body content of the page.

        :param url: (str) URL of the page.
        :return: (str) Body content of the page.
        """
        if url:
            self.set_url(url)
        return self.extract_body_content(self.get_page())

    def get_body_without_scripts(self, url=None):
        """
        Get the body content of the page without <script>...</script> tags.

        :param url: (str) URL of the page.
        :return: (str) Body content of the page without <script>...</script> tags.
        """
        if url:
            self.set_url(url)
        return self.remove_script_tags(self.get_body())

    def get_page_content(self, url):
        """
        Get the page content from the url.

        :param url: (str) URL of the page.
        :return: (str) HTML content of the page.
        """
        self.driver.get(url)

        WebDriverWait(self.driver, 10).until(EC.presence_of_element_located((By.TAG_NAME, "body")))

        start_time = time.time()
        while True:
            network_activity = self.driver.execute_script(
                "return window.performance.getEntriesByType('resource').filter(item => "
                "item.initiatorType == 'xmlhttprequest' && item.duration == 0)"
            )
            if not network_activity or time.time() - start_time > 30:
                break

        content = self.driver.page_source
        self.driver.close()
        self.driver.quit()

        return content

    @staticmethod
    def extract_body_content(html_content):
        """
        Extract the body content from the html_content.

        :param html_content: (str) HTML content of the page.
        :return: (str) Body content of the page.
        """
        soup = BeautifulSoup(html_content, "html.parser")
        body_content = soup.body

        return str(body_content)

    @staticmethod
    def remove_script_tags(input_content):
        """
        Remove all <script>...</script> tags from the input_content.

        :param input_content: (str) HTML content of the page.
        :return: (str) Body content of the page without <script>...</script> tags.
        """
        pattern_1 = re.compile(r"<script.*?>.*?</script>", re.DOTALL)
        pattern_2 = re.compile(r"<path.*?>.*?</path>", re.DOTALL)
        output = re.sub(pattern_1, "", input_content)
        output = re.sub

Обратите внимание, что некоторые сервисы уже предоставляют возможности доступа к web для LLM. В том числе, эта функция уже анонсирована и включена для ChatGPT для коропоративных и plus аккаунтов. Но, в конце концов, вам никто не мешает использовать свой код в целях экономии или оптимизации.

Также нам нужен такой runner, который будет получать данные для обратной связи ИИ о выполненных тестах. Допустим, будем использовать pytest-json-report плагин. В репорт сложим информацию о пройденных и упавших тестах, для каждого упавшего теста добавим саму ошибку (если нужно, то может туда и запихнуть трейсбэк), а так же будет неплохо обратно отдавать код страницы на момент падения. Это будет полезно, чтобы ИИ видел, что изменилось на странице, чтобы он мог написать правильные ожидаемые значения. Правда, в реальной жизни, страницы не такие уж и маленькие, и отдавать на каждую ошибку страницу - слишком жирно. Так что мы всё равно упрёмся в размер контента, и сравнительно немаленькую стоимость в токенах такого эксперимента. Но, по крайней мере, работа ИИ стоит центы, а не сотни долларов, как человека-джуна.

pytest_runner.py
import io
import json
from os import remove

import pytest

from utils.page_retriever import PageRetriever


def run_tests(test_files, add_failed_html=True, add_failure_reasons=True, count_of_htmls=1):
    """
    Run tests and return results in JSON format.

    :param test_files: (list) list with test files.
    :param add_failed_html: (bool) boolean to add html report.
    :param add_failure_reasons: (bool) boolean to add failure reasons.
    :param count_of_htmls: (int) count of htmls to add. Doesn't recommend to use more than 1.
    :return: JSON with results.
    """
    pytest.main(
        [
            "-q",
            "--json-report",
            "--json-report-file=test_report.json",
            #"-n=4",
            "-rfEx --tb=none -p no:warnings -p no:logging",
        ]
        + test_files
    )

    with open("test_report.json", encoding="utf-8") as json_file:
        data = json.load(json_file)

    results = {"passed": [], "failed": [], "error": [], "failure details": {}, "failed_pages": {}}

    for test in data["tests"]:
        node_name = test["nodeid"].split("::")[1]
        if test["outcome"] == "passed":
            results["passed"].append(node_name)
        elif test["outcome"] == "failed" or test["outcome"] == "error":
            results[test["outcome"]].append(node_name)
            if add_failure_reasons:
                results["failure details"][node_name] = {node_name: test["call"]["crash"]}
            if add_failed_html:
                if len(results["failed_pages"]) < count_of_htmls:
                    results["failed_pages"][node_name] = {node_name: parse_error_page(node_name)}

    json_results = json.dumps(results)

    return json_results


def parse_error_page(node_name):
    """
    Parse error page.

    :param node_name: (str) name of the node.
    :return: (str) formatted content of the page.
    """
    parser = PageRetriever()
    try:
        file_name = f"{node_name}.html"
        with open(file_name, "r", encoding="utf-8") as file:
            formatted_content = parser.remove_script_tags(parser.extract_body_content(file))
        remove(file_name)
        return formatted_content
    except io.UnsupportedOperation:
        return "No page available."

Дальше добавляем функции и json для chatgpt, которые будут звать pageretriever и runner соответственно.

gpt_functions.py
from examples.test_generator.pytest_runner import run_tests
from utils.page_retriever import PageRetriever

doc_engine = PageRetriever()
gpt_functions = [
    {
        "name": "get_page_code",
        "description": "Get page code to generate locators and tests",
        "parameters": {
            "type": "object",
            "properties": {"url": {"type": "string", "description": "The URL of the page to get the code from"}},
            "required": [],
        },
    },
    {
        "name": "get_tests_results",
        "description": "Get the results of the tests",
        "parameters": {
            "type": "object",
            "properties": {
                "test_files": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "The list of test files to run",
                }
            },
            "required": [],
        },
    },
]

gpt_functions_dict = {
    "get_page_code": doc_engine.get_body_without_scripts,
    "get_tests_results": run_tests,
}

Так как мы ожидаем, что ИИ будет возвращать только тесты и page object'ы, а не целиком файлы (так мы тоже чуть экономим токены), то нам надо это взять на себя, и написать класс, который будет пересоздавать файл каждый раз. Звать будем его сами, но, могли бы попросить это делать и ИИ, или даже отдельно сделать вариацию, чтобы “шапку” создавала для тестов\po нам она же. Здесь можно и дальше улучшать - добавить логику обновления файла, а не пересоздавать, добавления тестов к существующим и так далее. Но для эксперимента нам хватит просто пересоздания файла целиком. 

pom_case_generator.py
import os

from urllib.parse import urlparse, unquote


class PomTestCaseGenerator:
    """Class for generating test files and page objects from json data"""

    def __init__(self, url=""):
        """
        General init.

        :param url: (str) URL of the page.
        """
        self.url = url

    def set_url(self, url):
        """
        Set the url.

        :param url: (str) URL of the page.
        """
        self.url = url

    def ___create_pom_file(self, file_name, page_objects, url="", pom_folder="pom"):
        """
        Create page object model file.

        :param file_name: (str) Name of the file.
        :param page_objects: (list) List of page objects.
        :param url: (str) URL of the page.
        :param pom_folder: (str) Folder for page object model files.
        """
        if not url:
            url = self.url
        if not os.path.exists(pom_folder):
            os.makedirs(pom_folder)
        with open(f"{pom_folder}/page_{file_name}.py", "w", encoding="utf-8") as pom_file:
            pom_file.write("from selenium.webdriver.common.by import By\n")
            pom_file.write("from selenium.webdriver.support.ui import WebDriverWait\n")
            pom_file.write("from selenium.webdriver.support import expected_conditions as EC\n\n\n")
            pom_file.write(f'class Page{"".join(word.capitalize() for word in file_name.split("_"))}:\n')
            pom_file.write("    def __init__(self, driver):\n")
            pom_file.write(f'        self.url = "{url}"\n')
            pom_file.write("        self.driver = driver\n\n")
            for method in page_objects:
                pom_file.write(f"    {method}\n\n")

    @staticmethod
    def ___create_test_file(file_name, tests, pom_folder="pom", tests_folder="tests"):
        """
        Create test file.

        :param file_name: (str) Name of the file.
        :param tests: (list) List of tests.
        :param pom_folder: (str) Folder for page object model files.
        :param tests_folder: (str) Folder for test files.
        """
        with open(f"{tests_folder}/test_{file_name}.py", "w", encoding="utf-8") as test_file:
            test_file.write("import pytest\n\n")
            test_file.write(
                f'from {pom_folder}.{os.path.splitext(f"page_{file_name}")[0]} import Page'
                f'{"".join(word.capitalize() for word in file_name.split("_"))}\n\n\n'
            )
            test_file.write('@pytest.fixture(scope="function")\n')
            test_file.write("def page(driver):\n")
            test_file.write(
                f'    page_under_test = Page{"".join(word.capitalize() for word in file_name.split("_"))}(driver)\n'
            )
            test_file.write("    driver.get(page_under_test.url)\n")
            test_file.write("    return page_under_test\n\n\n")
            for test in tests:
                test_file.write(f"{test}\n\n\n")

    def create_files_from_json(self, json_data, url="", pom_folder="pom", tests_folder="tests"):
        """
        Create test and page object model files from json data.

        :param json_data: (str) JSON data.
        :param url: (str) URL of the page.
        :param pom_folder: (str) Folder for page object model files.
        :param tests_folder: (str) Folder for test files.
        """
        if not url:
            url = self.url
        parsed_url = urlparse(unquote(url))
        file_name = parsed_url.path.strip("/").replace("/", "_") or "index"
        self.___create_test_file(file_name, json_data["tests"], pom_folder="..pom", tests_folder=tests_folder)
        self.___create_pom_file(file_name, json_data["page_objects"], url, pom_folder=pom_folder)

Наконец, когда у нас есть все части, просто позовём всё в правильном порядке.

generator_test.py
import asyncio
import json
import logging

from examples.creds import oai_token, oai_organization
from examples.test_generator.gpt_functions import gpt_functions, gpt_functions_dict
from examples.test_generator.pom_case_generator import PomTestCaseGenerator
from openai_api.src.openai_api import ChatGPT
from openai_api.src.openai_api.logger_config import setup_logger

PAGE_UNDER_TEST = "https://www.saucedemo.com/"
generator = PomTestCaseGenerator(url=PAGE_UNDER_TEST)


def setup_gpt():
    """Setup GPT bot with appropriate functions and settings"""
    gpt = ChatGPT(auth_token=oai_token, organization=oai_organization, model="gpt-4-0613")
    gpt.function_dict = gpt_functions_dict
    gpt.function_call = "auto"
    gpt.functions = gpt_functions
    gpt.system_settings = """%instructions_inside%"""
    return gpt


async def main():
    """Main function for testing GPT bot"""
    print("===Setup GPT bot===")
    gpt = setup_gpt()
    
    print(f"===Get page code of {PAGE_UNDER_TEST} and generate PO and tests===")
    response = await anext(gpt.str_chat(f"Get page code of {PAGE_UNDER_TEST} and generate PO and tests"))
    print(response)  # print response as debug output
    response = response.replace("\n", "")  # need to proper json loading
    generator.create_files_from_json(
        json.loads(response), pom_folder="examples/test_generator/pom", tests_folder="examples/test_generator/tests"
    )
    print("===Get tests results for examples/test_generator/tests/test_index.py==")
    response = await anext(gpt.str_chat("Get tests results for examples/test_generator/tests/test_index.py"))
    print(response)  # print response as debug output
    print("===If there are failures in code, please fix it by fixing POM and tests===")
    response = await anext(gpt.str_chat("If there are failures in code, please fix it by fixing POM and tests"))
    print(response)  # print response as debug output
    generator.create_files_from_json(
        json.loads(response.replace("\n", "")), pom_folder="..pom", tests_folder="examples/test_generator/tests"
    )
    # repeat as many times as you wish or getting 100% passrate


asyncio.run(main())

Получение страницы, генерация и запуск тестов занимают полторы минуты (с перегенерацией на основе ошибок, т.е. весь цикл), что неплохо по времени для одной итерации. Результаты генерации “вслепую” выглядят достаточно неплохо. Да, возможно было бы написать больше тестов, но в целом сеть написала базовые сценарии, да и тесты выглядят правильными. Так, например, в первой итерации у нас прошёл один из пяти тестов. Правда, не совсем честно.

Неплохо, но где же assert в первом тесте? Разве это тест?
Неплохо, но где же assert в первом тесте? Разве это тест?

Зато во второй итерации ИИ исправил и первый, нечестный тест, и поправил тесты на основании результатов запуска. Теперь у нас проходят пять из шести тестов. Всего за 3 минуты!

5 из 6 - без моего участия за три минуты
5 из 6 - без моего участия за три минуты

Как видно, подход работает, и перебирая страницу за страницей можно быстро создавать рабочие тесты. С учётом того, если у вас есть требования или документация, что всё-таки является более правильным подходом, то дополняя запрос к ИИ, а так же уточняя, какие сценарии вы хотите видеть (Негативные сценарии? Граничные значения?) можно добиваться очень неплохих результатов в полу-автоматическом режиме, лишь передав набор url в скрипт.

Выводы

Я не представляю свою жизнь и дальнейшую работу без ИИ. Использование ИИ в моей работе повышает производительность на порядок. Надеюсь, с развитием технологии, возможности и отдача так же возрастёт без роста цены, и многие вещи, сегодня требующие участия и вовлечения будут так же делегированы ИИ.

Означает ли это, что тестировщики больше не нужны? Нет. Несмотря на все замечательные описательные и генеративные возможности ИИ - это всего лишь мощный инструмент, если угодно станок, которым тоже нужно уметь пользоваться, а при неумении можно отрубить себе пальцы. 

Железный конь идёт на смену крестьянской лошадке!
Железный конь идёт на смену крестьянской лошадке!

Означает ли это, что джуны не нужны? И да, и нет. На возню что с джунами, что с ИИ всё равно нужно тратить время. И с теми, и с другими, вырастив, получаешь отдачу - и рабочую силу, и моральное удовлетворение, и высвобождение времени. ИИ быстро учится, но под каждую задачу требует перенастройки. Люди в этом отношении более гибкие и универсальные, но и более медленные. На мой взгляд, внедрение ИИ повысит планку для вхождения в профессию, но при этом сделает это вхождение более быстрым и результативным, так как у джунов так же есть доступ к этому инструменту. Уменьшится количество людей, требуемых в команде.

Согласно прогнозам, из-за внедрения ИИ до 300 миллионов человек могут потерять свою работу
Согласно прогнозам, из-за внедрения ИИ до 300 миллионов человек могут потерять свою работу

Отвечая на вопрос HR и нанимающих менеджеров: “надо увольнять и ждать сокращений”? Если человека может заменить ИИ сейчас, то у меня встречный вопрос к вам: зачем вы держите таких людей? Зачем вы их нанимали? Хорошего тестировщика, пусть даже джуна, решившего освоить профессию, не заменит инструмент, как рабочих на заводе не заменили станки. Просто суть рабочих изменилась - они научились обращаться со станками. Если человек понимает что есть его профессия, хочет развиваться - читая книги и\или общаясь с ИИ, то любой инструмент в его руках и вклад в команду от этого человека будет выше, чем у ИИ, просто из-за его амбиций и стремлений, любопытства, мотивации и пытливости. Если это человек, которого, как ИИ нужно просить каждый раз открывать и делать, но при этом ждать результата не минуту, а неделю - тогда ответ очевиден.

ИИ, хотите ли вы этого или нет - это и ваш инструмент, и ваш коллега
ИИ, хотите ли вы этого или нет - это и ваш инструмент, и ваш коллега

Решает ли любой вопрос ИИ сегодня? Нет, как и требует постоянного контроля и перепроверки, причём компетентным человеком. С другой стороны, позволяет очень быстро решать быстрые, часто рутинные вопросы - на долгосрок. Грубо говоря, если у вас есть заказчик, для которого нужно что-то быстро сделать, но не обязательно качественно:  получить какое-то тестовое покрытие и smoke-модель из разряда “проверь, что оно работает”, то это можно сделать за минуты. И, в конце концов сконцентрироваться на важных вещах в работе и вне её. Пусть поработают роботы.

На этом всё. Если этот материал был вам полезен, то не забудьте поставить лайк этому посту, написать комментарий, а так же, если воодушевитесь - поделиться монеткой. Оригинал статьи тут.

Комментарии (13)


  1. MAXH0
    23.10.2023 08:49
    +6

    ИИ не конкурирует с людьми! ИИ не субъект... Люди при помощи ИИ конкурируют с людьми. Точно так же, как не станки разорили ткачей. И не овцы съели английских крестьян. Это сделали английские лорды.

    Вопрос о свободном доступе трудящихся (в дан ном случае тестировщиков) к средствам производства.


    1. wwakabobik Автор
      23.10.2023 08:49

      Я полностью согласен.

      Но есть одно но: глубина и масштабность. Сейчас технологии развиваются быстро. И то, что сетка не могла два года назад, сегодня она с лёгкостью выполняет. Условно говоря, сделайте паука, который по коммиту\деплою будет обходить код\сайт\приложение\таск-менеджер и править тесты. Работа тестировщика изменится, ручные операции будут отмирать, но появятся новые обязанности, по обслуживанию и модернизации этого станка-паука.


      1. MAXH0
        23.10.2023 08:49

        Работа определенно изменится.

        НО тут мы можем столкнуться с тем, что профессиональные инструменты станут дороги и придавлены копирайтами. Это сейчас вы платите 10$ и помогаете обучать сторонний сервис. Но сторонний сервис не давал вам гарантий, что цена сохранится на прежнем уровне и будет доступна по первому требованию...

        Поэтому только НЕЗАВИСИМЫЙ ИИ, который вы можете запустить на локальном компьютере даст вам гарантию того, что вы будете независимы САМИ.


        1. wwakabobik Автор
          23.10.2023 08:49

          Как бы я не любил брать всё на себя, но чаще всего всё работает не так.
          Редкие компании покупают железные сервера, а используют облачные решения.

          Чтобы развернуть ИИ мне, всё-таки нужна вменяемая мощность и GPU. Это не разорительно дорого, но это и сравнительно немалая изначальная инвестиция, плюс ещё электричество, сетка\интернет, амортизация, время своё\сотрудников. То есть, как правило, услуги сервисов если не дешевле, то сопоставимы с этими затратами. А к моменту равной стоимости, возможно, уже пора будет обновляться.

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


          1. MAXH0
            23.10.2023 08:49
            +1

            Ключевое слово "вы можете"... Пока есть независимые свободные решения, на которые вы можете уйти - это гарантия вашей свободы. Многие об этом забывают. И думают, что если они не пользуются свободным ПО, то свободное ПО им не помогает.

            Это не так. Недавний скандал с ЮНИТИ прекрасно показал, что вменяемая лицензия сейчас != вменяемая лицензия завтра. НО если есть свободные альтернативы, то жадность корпоратов быстро лечится.

            То же самое и нейросети. Надо следить чтобы нейросети не закрыли патентами и копирайтами и развивались свободные альтернативы. А пока альтернативы есть, то пользоваться или нет ими СВОБОДНЫЙ выбор каждого.


      1. vlad4kr7
        23.10.2023 08:49

        Почему вы решили, что всегда надо будет править тесты, а не код?


        1. wwakabobik Автор
          23.10.2023 08:49

          Зачем править код, если он работает?

          Если изменились требования, пусть даже просто обновилась какая-то зависимость и вы, как тестировщик, будете делать регресс - вы всё равно пересматриваете тесты. Возможны варианты для тестировщика:
          а) ничего не поменялось
          б) нужно поменять тест (описание, ручной тест), но не менять код автотеста
          в) тест не нужно менять, но нужно изменить код автотеста
          г) всё поменялось, всё нужно обновлять.

          Или вы больше про терминологию? Тест - это тест, не важно, автотест это или нет. Лично для меня они зеркальны, так как обычно большая часть функциональности покрыта, и танцую от автоматизации, а не дельты непокрытой.


          1. vlad4kr7
            23.10.2023 08:49
            +1

            А то, что код может работать НО не правильно, вы вообще не учитываете?

            Обычно, тестер находит ошибку и создает тикет. Реже, приходится править тест.


            1. wwakabobik Автор
              23.10.2023 08:49

              Разве это не "нужно изменить код автотеста"?

              Или вы о генераторе? Если о нём, то вы разве не делаете code review в своей команде?


  1. ValeriaFatekhova
    23.10.2023 08:49

    Я правильно понимаю: вы скармливаете стороннему сервису внутреннюю документацию проекта? Ваши безопасники ещё кол для вас затачивать не начали?)))


    1. wwakabobik Автор
      23.10.2023 08:49

      Вы сказали что-то на энтерпрайзовском :)

      Почему код страницы, который (у)видят клиенты, или public api - это внутренняя документация?


      1. ValeriaFatekhova
        23.10.2023 08:49

        потому что код и UI интерфейс - это разные вещи. И код - это интелектуальная собственность вашей компании, если у вас не опенсорс, вряд ли они хотели бы делиться с кем то своим кодом. И точно ли вы уверены, что у вас public api? Паблик - это когда любой сторонний разработчик может встраивать ваши функции в свои приложения. В целом, дело ваше:) Главное убедитесь, что данные, которые вы отдаете ИИ не под дна и вам однажды не прилетит за это


        1. wwakabobik Автор
          23.10.2023 08:49

          Всё так. API есть и внутренний и публичный. Внутренний есть закрытый и открытый, последний вы можете при желании реверснуть, чтобы написать тесты\json а-ля swagger вслепую :)

          А в чём замечание? То, что куски, которые сгенерирует ИИ будут в коде тестов? А может и не будут. А какой смысл код [тестов\приложения] скармливать ИИ?

          Или вы вообще не про генератор, а про тот же Copilot - который "объясни мне" видит кусок какой-то функции? Тогда тут больше тогда вопрос дискуссионный, кому, и что вы всё-таки доверяете. Доверяете ли ОС, которой пользуетесь, ведь она шпионит за вами? Доверяете ли стране, в которой живёте, ведь она шпионит за вами? Доверяете ли инструментами и сервисами, что пользуетесь, ведь они шпионят за вами? Будете ли поднимать свою LLM на своём железе? Будете ли не пользоваться технологиями, которые дают преимущества? Да, наверное, это вопросы к политике компании и вам, как участнику.