Введение

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

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

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

Решение проблемы: как я исправил

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

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

Еще одна проблема ручного тестирования — высокая вероятность человеческой ошибки. Несмотря на все усилия тестировщиков, всегда существует риск пропуска критических дефектов либо из-за недосмотра, либо из-за недостаточного покрытия тестами. Кроме того, ручное тестирование склонно к субъективности, а это означает, что разные тестировщики могут иметь разные мнения о том, правильно ли работает конкретная функция или функция.

Давайте перейдем к коду

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

  1. Определите масштаб своего тестового бота: прежде чем приступить к созданию своего бота, вам необходимо определить, какие задачи он будет выполнять. Это поможет вам определить, какие инструменты и технологии вам нужно использовать, и какую структуру вы должны создать.
  2. Выберите свой язык программирования: я решил использовать Python в качестве языка программирования, потому что в нем есть много отличных библиотек и инструментов для веб-скрейпинга и автоматизации.
  3. Выберите свой инструмент веб-автоматизации: я использовал Selenium WebDriver для взаимодействия с веб-сайтами и автоматизации задач тестирования. Selenium — популярный инструмент с открытым исходным кодом для веб-автоматизации и тестирования. еще одна хорошая вещь, которую имеет Selenium, это то, что он может работать без головы, и вы можете поместить их в контейнеры Docker.
  4. Напишите свой код: я начал с написания кода для инициализации Selenium WebDriver и перехода на веб-сайт, который я хотел протестировать. Оттуда я использовал Selenium API для взаимодействия с веб-сайтом, нажатия кнопок, заполнения форм и чтения данных.
  5. Внедрение тестовых случаев: как только я написал базовый код, я начал реализовывать конкретные тестовые случаи. Это включало написание кода для имитации различных сценариев, таких как правильное заполнение формы, заполнение формы с ошибками или нажатие различных кнопок на веб-сайте.
  6. Отладка и устранение неполадок. По пути я столкнулся с рядом проблем, таких как изменения веб-сайта или непредвиденные ошибки. Чтобы преодолеть это, я использовал инструменты отладки и устранения неполадок, встроенные в Python и Selenium, а также другие библиотеки с открытым исходным кодом.
  7. Тестируйте и уточняйте: как только мой бот был запущен и запущен, я начал использовать его для тестирования веб-сайта. Я внес изменения в код по мере необходимости, чтобы повысить его точность и эффективность.

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

"""
This is a snippt code which tries to run a function n times and if it ouccurd
an error, it will log every step, take an screen shot and etc ...
"""
MAX_TRY_CNT = 10 # Numbers to try a work
class Surfer:
    @staticmethod
    def try_it(func):
        def inner(self):
            t0 = datetime.now()
            func_name = func.__name__.replace("_", ' ')
            try_cnt = 0
            while try_cnt <= MAX_TRY_CNT:
                try:
                    logger.info(f"Trying to {func_name} - {try_cnt} [Info]")
                    output = func(self)
                except Exception as e:
                    #self.take_screenshot(func_name)
                    logger.warning(f"{e} Happened")
                    pass
                else:
                    t1 = datetime.now()
                    logger.info(f"Succeed {func_name} [Success]")
                    return output, (t1 - t0).seconds
                time.sleep(1)
                try_cnt += 1
            else:
                logger.error(f"Max try captured while {func_name} {try_cnt} >= {MAX_TRY_CNT} [Error]")
                self.take_screenshot(func_name)
                self.driver.delete_all_cookies()
                self.driver.close()
                raise SurferException()

        return inner

Этот код определяет класс Surfer со статическим методом try_it и методом экземпляра take_screenshot. Метод try_it принимает функцию в качестве аргумента и возвращает новую функцию, которая оборачивает исходную функцию с обработкой ошибок и логикой повторных попыток. Если обернутая функция завершается успешно, возвращаются выходные данные и затраченное время. Если это не удается после максимального количества попыток, делается снимок экрана, браузер закрывается и поднимается SurferException.

Метод take_screenshot принимает имя функции в качестве аргумента и делает снимок экрана с помощью метода save_screenshot атрибута driver экземпляра Surfer.

Обратите внимание, что код ссылается на MAX_TRY_CNT переменную, которая не определена в предоставленном фрагменте кода, поэтому предполагается, что она определена в другом месте.

теперь давайте посмотрим на некоторые функции и действия в Snapp! Экспресс простой поток:

@try_it
def _fill_basket(self):
    """
    Fill basket
    :return:
    # TODO: Handle if the category doesn't have enough product
    """
    # IF vendor is closed, choose another time delivery
    try:
        self.driver.find_element(By.XPATH, '//*[@id="modal-root"]/div[13]/div/div[3]/button').click()
    except Exception:
        pass

    self._scroll()
    buttons = self.driver.find_elements(By.TAG_NAME, 'button')
    for button in buttons:
        self._clear()
        if button.text == "افزودن":
            button.click()

    self.sleep(0.5)

    try:
        check_out = self.driver.find_element(By.XPATH, '//*[@id="pr-2"]/div/div[3]/button')
    except Exception:
        raise Exception("Basket is not fulled yet...")

@try_it
def _go_to_checkout(self):
    if self.driver.current_url == "<URl of Checkout>":
        return  True

    check_out = self.driver.find_element(By.XPATH, '//*[@id="pr-2"]/div/div[3]/button')
    check_out.click()

@try_it
def _go_to_payment(self):

    # Continue
    continue_button = self.driver.find_element(By.XPATH, '//*[@id="pr-2"]/div/div/button')
    continue_button.click()

    if self.driver.current_url != '<ULR of payment page>':
        raise SurferException("Didn't got payment")

@try_it
def _go_to_payment_gateway(self):
    # Go to payment
    self.sleep(2)
    old = self.driver.current_url
    payments = []
    try:
        payments.append(self.driver.find_element(By.XPATH, '//*[@id="payment-bank-AP_Web"]'))
    except Exception:
        pass
    try:
        payments.append(self.driver.find_element(By.XPATH, '//*[@id="payment-bank-saman"]'))
    except Exception:
        pass
    try:
        payments.append(self.driver.find_element(By.XPATH, '//*[@id="payment-bank-parsian"]'))
    except Exception:
        pass

    self._clear()
    random_gateway = random.choice(payments)
    gateway_name = random_gateway.text.split('\n')[1]

    logger.info(f"Choosing a payment randomly ->  {gateway_name}")

    random_gateway.click()

    self.sleep(2)

    payment = self.driver.find_element(By.XPATH, '//*[@id="payment-submitOrder"]')
    payment.click()

    self.sleep(3)

    if self.driver.current_url == old:
        raise Exception("Didn't got payment gateway")

@try_it
def _cancel_order_payment(self):
    # Cancel payment
    cancel_pay = None
    old = self.driver.current_url
    try:
        self.driver.find_element(By.XPATH, '//*[@id="cancel-btn"]').click()
    except Exception:
        pass
    try:
        self.driver.find_element(By.XPATH, '//*[@id="button-payment-cancel"]').click()
    except Exception:
        pass
    try:
        self.driver.find_element(By.XPATH, '//*[@id="btnCancel"]').click()
    except Exception:
        pass

    self.sleep(3)
    if self.driver.current_url == old:
        raise Exception("Didn't canceled from payment gateway")

    #self.take_screenshot("Success")

@try_it
def _choose_address(self):
    """
    Choose address if needed
    :return: None
    """
    self.driver.find_element(By.XPATH, EL.ADDRESS_BTN).click()

Этот код представляет собой класс Python с именем Surfer, который, по-видимому, является реализацией веб-скребка с использованием Selenium. Он содержит статический метод с именем try_it, который принимает другую функцию в качестве аргумента и возвращает новую функцию, являющуюся оболочкой исходной. Эта новая функция имеет блок try-except, который пытается вызвать исходную функцию и возвращает ее результат в случае успеха. Если исходная функция вызывает исключение, новая функция регистрирует предупреждающее сообщение и повторяет исходную функцию до тех пор, пока не будет достигнуто максимальное количество попыток или пока функция не завершится успешно.

В классе есть метод экземпляра с именем ensure_driver, который возвращает экземпляр webdriver.Chrome. Этот метод использует статическую переменную с именем DRIVER для хранения ссылки на экземпляр драйвера, если он был создан ранее; в противном случае он создает новый экземпляр, используя конструктор webdriver.Chrome и объект chrome_options.

Другие методы класса включают _get_url, _set_login_cookie, _choose_vendor, _scroll, _clear, _choose_random_category и другие, которые выполняют различные задачи, используя Selenium для взаимодействия с веб-страницей. Все эти методы используют декоратор try_it для обработки исключений и повторного запуска функции при необходимости.

Наконец, у класса есть статический метод sleep, который приостанавливается на случайное время от 3 до 5 секунд, и метод take_screenshot, который делает снимок экрана текущей веб-страницы и сохраняет его в файл.

Вы также можете создавать свои действия, используя селекторы классов XPATH или CSS,

XPath — это язык для выбора узлов в документе XML, который обычно используется для поиска элементов в документах HTML. Он использует синтаксис пути для обхода дерева HTML и идентификации элементов на основе их атрибутов и положения относительно других элементов.

Вот пример использования выражения XPath для поиска определенного элемента кнопки на веб-странице:

from selenium import webdriver

driver = webdriver.Chrome()
driver.get("https://www.example.com")

# Locate the button element using an XPath expression
button = driver.find_element_by_xpath("//button[@id='submit-button']")

# Click the button
button.click()

# Close the browser window
driver.quit()

В этом примере выражение XPath "//button[@id='submit-button']" выбирает элемент кнопки с атрибутом id, равным submit-button. Метод find_element_by_xpath возвращает первый элемент, соответствующий выражению XPath.

Получите результат, используя Grafana, Prometheus и Dockerize проект

Докеризация проекта:

так как у нас есть метод _ensure_driver, и мы добавили в наш драйвер безголовые и графические опции:

@try_it
def ensure_driver(slef):
    """
    Ensure driver
    :return:
    """
    from selenium.webdriver.chrome.options import Options
    options = Options()
    options.add_argument('--headless')
    options.add_argument("--no-sandbox")
    options.add_argument("--window-size=1920,1080")
    options.add_argument("--disable-setuid-sandbox")
    options.add_argument("--remote-debugging-port=9222")
    options.add_argument("--disable-dev-shm-using")
    options.add_argument("--disable-extensions")
    options.add_argument("--disable-gpu")
    options.add_argument("start-maximized")
    options.add_argument("disable-infobars")
    if not DRIVER:
        try:
            return webdriver.Chrome(options=options, executable_path="./chromedriver")
        except Exception:
            return webdriver.Chrome(options=options, service=Service(ChromeDriverManager().install()))
    return DRIVER

Затем функция проверяет, определена ли уже глобальная переменная DRIVER. Если да, то возвращается существующий экземпляр драйвера. В противном случае функция пытается создать новый экземпляр драйвера, используя класс webdriver.Chrome. Если это не удается, он пытается создать новый экземпляр драйвера, используя класс ChromeDriverManager из модуля webdriver_manager, который автоматически загрузит и установит соответствующую версию исполняемого файла ChromeDriver.

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

это может помочь нам запустить код в контейнере докеров:

FROM python:latest

WORKDIR /code


RUN curl https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb --output google-chrome-stable_current_amd64.deb
RUN curl https://chromedriver.storage.googleapis.com/109.0.5414.74/chromedriver_linux64.zip --output chromedriver_linux64.zip

COPY . .

RUN unzip chromedriver_linux64.zip

RUN apt-get update && apt-get install -y ./google-chrome-stable_current_amd64.deb


RUN pip install -U pip
RUN pip install -r requirements.txt

CMD ["python", "main.py"]

Этот Dockerfile загружает последнюю версию Python, устанавливает рабочий каталог /code, а затем переходит к загрузке стабильной версии Google Chrome и соответствующей версии ChromeDriver для Linux.

Далее он копирует текущий каталог (коды, драйвер) в рабочий каталог.

Затем команда unzip используется для извлечения zip-файла Chromedriver.

apt-get используется для установки Google Chrome.

Команда pip используется для обновления pip до последней версии и последующей установки требований, указанных в requirements.txt.

Наконец, контейнер Docker получает указание запустить файл main.py с помощью Python.

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

Логи!

Теперь мы на стороне докера, мы можем легко отправить наши журналы на сервер Graylog с помощью Gelf:

logging:
  driver: gelf
  options:
    gelf-address: tcp://172.20.20.20:12201
    tag: surfer

В конфигурации указаны следующие параметры:

  • gelf-address: указывает адрес удаленного сервера, на который должны отправляться журналы. В этом случае сервер находится по IP-адресу 172.20.20.20 и прослушивает порт 12201.
  • tag: Этот параметр добавляет тег к журналам для их идентификации. В этом случае тегу присваивается значение surfer.

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

Прометей!

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

давайте создадим файл с именем prometheus.py

from prometheus_client import Summary, Counter, Histogram, Info, Gauge
from prometheus_client import start_http_server, Info, CollectorRegistry, Metric
import os

REG = CollectorRegistry()
# Create a metric to track time spent and requests made.
SURF_COUNT = Counter('surf_count', "Surf Flow Count", registry=REG)
SUCCESS_COUNT = Counter('surfer_success_count', "Failed Flow Count", registry=REG)
VERSION = Info('surfer_info', 'surfer info', registry=REG)

choose_address_time = Gauge('choose_address_time', 'Time spent processing request', registry=REG)
choose_vendor_time = Gauge('choose_vendor_time', 'Time spent processing request', registry=REG)
fill_basket_time = Gauge('fill_basket_time', 'Time spent processing request', registry=REG)
go_to_checkout_time = Gauge('go_to_checkout_time', 'Time spent processing request', registry=REG)
go_to_payment_time = Gauge('go_to_payment_time', 'Time spent processing request', registry=REG)
go_to_payment_gateway_time = Gauge('go_to_payment_gateway', 'Time spent processing request', registry=REG)
cancel_order_payment_time = Gauge('cancel_order_payment_time', 'Time spent processing request', registry=REG)
full_time = Gauge('full_time', 'Time spent processing request', registry=REG)

# Export Google Chrome version
write_gcv_to_file = os.system("google-chrome --no-sandbox --version > /tmp/gcv.txt")
with open("/tmp/gcv.txt", "r") as f:
    data = f.read()

VERSION.info({
    "version": "0.0.2",
    "running_by": f"{data.strip()}",
})

Метрика VERSION Info используется для предоставления информации об используемой версии Google Chrome, а также об авторе, работающем пользователе и последней фиксации.

В целом, этот код полезен для мониторинга и анализа показателей производительности веб-скрапинга или процесса автоматизации.

Основной файл питона

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

def add_order():
    surfer = Surfer()
    times = surfer.add_an_order()
    # Set times
    choose_address_time.set(times['choose_address_time'])
    choose_vendor_time.set(times['choose_vendor_time'])
    fill_basket_time.set(times['fill_basket_time'])
    go_to_checkout_time.set(times['go_to_checkout_time'])
    go_to_payment_time.set(times['go_to_payment_time'])
    go_to_payment_gateway_time.set(times['go_to_payment_gateway_time'])
    cancel_order_payment_time.set(times['cancel_order_payment_time'])
    full_time.set(times['full_time'])


def main():
    start_http_server(9008, registry=REG)
    i = 1
    logger.info(f"Starting the {i} Job")
    while True:
        try:
            SURF_COUNT.inc(1)
            add_order()
        except Exception as e:
            logger.critical(f"{e}")
            logger.critical(f"Can't Run the {i} Job, Try again!")
            pass
        else:
            i += 1
            SUCCESS_COUNT.inc(1)
            logger.info(f"Sleeping {RUN_INTERVAL} Seconds...")
            time.sleep(RUN_INTERVAL)

Функция add_order() использует экземпляр класса Surfer для добавления заказа и возвращает словарь времени, затраченного на каждый шаг процесса. Затем время устанавливается для соответствующих датчиков с использованием метода set().

Функция main() запускает HTTP-сервер Prometheus на порту 9008 и запускает бесконечный цикл для непрерывного добавления заказов с помощью функции add_order(). Цикл также увеличивает счетчик SURF_COUNT на 1 для каждой итерации и увеличивает счетчик SUCCESS_COUNT на 1, если функция add_order() выполняется успешно. Если во время выполнения функции add_order() возникает исключение, оно регистрируется в журнале, и цикл продолжается. Цикл приостанавливается на RUN_INTERVAL секунд между каждой итерацией.

как вы видите, он создаст HTTP-сервер и покажет его показатели, поэтому добавьте его в качестве цели в конфигурацию очистки Prometheus, например:

  - job_name: 'surfer'
    metrics_path: '/metrics'
    static_configs:
      - targets:
          - "localhost:9008"

В этом случае имя задания — surfer, а путь к метрикам — /metrics. В разделе static_configs указаны цели, к которым можно получить доступ к метрикам. В этом случае указана только одна цель с IP-адресом 172.30.30.40 и портом 9008, где сервер Prometheus может собирать метрики.

Графана!

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

Я создал панель инструментов для своих показателей, таких как:

это время и количество серфинга, которые помогли мне найти ошибки и медлительность в каждой части действия

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

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

Используя как снимки экрана, так и журналы, разработчики могут получить более полное представление о программном потоке и проблемах, которые могут возникнуть. Это может помочь им быстрее и эффективнее выявлять и устранять ошибки.

Заключение

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

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

Тем, кто рассматривает возможность внедрения автоматизированного тестирования, мы рекомендуем начать с Python и Selenium из-за их популярности и простоты использования. Важно тщательно спланировать тесты и расставить приоритеты, чтобы обеспечить тщательное тестирование наиболее важных компонентов приложения. Кроме того, регулярная проверка и обновление тестов для отражения изменений в приложении поможет поддерживать эффективность автоматизированного процесса тестирования.

В целом внедрение автоматизированного тестирования с помощью Python и Selenium стало ценным опытом, который повысил качество и эффективность нашего процесса тестирования.

Спасибо за прочтение !

AE