Добрый день, уважаемые читатели. Меня зовут Виктор Буров. Я работаю разработчиком в компании ISPsystem и хочу поделиться опытом автоматизации тестирования.

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

Мы решили написать модуль создания автоматических тестов. Чтобы тестировщик мог просто нажать кнопку создания теста, выполнить условия тест-кейса и по окончании нажать «завершить» — и всё, тест был готов! Простая идея, но реализовать ее оказалось непросто. Потому что мы хотели, чтобы этот модуль был максимально адаптирован под наши продукты и использовал преимущество унифицированного интерфейса: чтобы сделанная запись выглядела как готовый тест-кейс. Это бы полностью избавило от ручной работы по написанию тестов. Получившаяся в итоге система получила название «магнитофон».


Интерфейс модуля просмотра условий тест-кейса

Принцип работы


Все параметры запросов (HTTP-заголовки, переменные окружения, POST-данные, если такие есть) и весь ответ записывается в xml-файл. Каждой записи присваивается порядковый номер. Все запросы разделяются на модифицирующие и не модифицирующие. После записи теста многие из немодифицирующих запросов вырезаются, так как не влияют на выполнение тестов и только затягивают и запутывают процесс выполнения (отсюда пропущенные порядковые номера на скриншоте).

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

При воспроизведении запросы отправляются напрямую в API приложения, не используя браузер.

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

Пример записи одного вызова, сделанного магнитофоном:

Запись
    <params>
      <param name="CONTENT_LENGTH">210</param>
      <param name="CONTENT_TYPE">application%2Fx%2Dwww%2Dform%2Durlencoded%3B%20charset%3DUTF%2D8</param>
      <param name="HTTPS">on</param>
      <param name="HTTP_ACCEPT">text%2Fhtml%2C%20%2A%2F%2A%3B%20q%3D0%2E01</param>
      <param name="HTTP_ACCEPT_LANGUAGE">en%2DUS%2Cen%3Bq%3D0%2E5</param>
      <param name="HTTP_CACHE_CONTROL">no%2Dcache</param>
      <param name="HTTP_CONNECTION">keep%2Dalive</param>
      <param name="HTTP_COOKIE">corelang5%3Dorion%3Aru%3B%20ispmgrlang5%3Dorion%3Aru%3B%20ipmgrlang5%3Dorion%3Aru%3B%20ipmgrses5%3Dbdd69179d627%3B%20ispmgrses5%3D14157f7bbc5e%3B%20menupane%3D30%5Faccount%2D1%253A30%5Fdomains%2D1%253A30%5Fwebserver%2D1%253A30%5Fantispam%2D1%253A30%5Fmaintain%2D1%253A30%5Ftool%2D1%253A30%5Fstat%2D1%253A30%5Fsrvset%2D1%253A30%5Fsysstat%2D1%253A30%5Fintegration%2D1%253A30%5Fset%2D1%253A30%5Fmgrhelp%2D1</param>
      <param name="HTTP_HOST">172%2E31%2E240%2E175%3A1500</param>
      <param name="HTTP_ISP_CLIENT">Web%2Dinterface</param>
      <param name="HTTP_PRAGMA">no%2Dcache</param>
      <param name="HTTP_REFERER">https%3A%2F%2F172%2E31%2E240%2E175%3A1500%2Fispmgr</param>
      <param name="HTTP_USER_AGENT">Mozilla%2F5%2E0%20%28X11%3B%20Ubuntu%3B%20Linux%20x86%5F64%3B%20rv%3A24%2E0%29%20Gecko%2F20100101%20Firefox%2F24%2E0</param>
      <param name="HTTP_X_REQUESTED_WITH">XMLHttpRequest</param>
      <param name="QUERY_STRING"/>
      <param name="REMOTE_ADDR"></param>
      <param name="REMOTE_PORT">38640</param>
      <param name="REQUEST_METHOD">POST</param>
      <param name="REQUEST_URI">%2Fispmgr</param>
      <param name="SCRIPT_NAME">%2Fispmgr</param>
      <param name="SERVER_ADDR">172%2E31%2E240%2E175</param>
      <param name="SERVER_NAME">172%2E31%2E240%2E175</param>
      <param name="SERVER_PORT">1500</param>
    </params>
<postdata>func%3Demaildomain%2Eedit%26elid%3D%26name%3Dtest%2Eemail%26owner%3Dusr%26ipsrc%3Dauto%26defaction%3Derror%26redirval%3D%26spamassassin%3Doff%26avcheck%3Doff%26clicked%5Fbutton%3Dok%26progressid%3Dfalse%5F1424243906672%26sok%3Dok%26sfrom%3Dajax%26operafake%3D1424243906673</postdata>
    <answer>
      <doc lang="ru" func="emaildomain.edit" binary="/ispmgr" host="https://172.31.240.175:1500" features="cba82687e7756e2c0195c88d4180f5d50" notify="0" theme="/manimg/orion/" css="main.css" logo="logo-ispmgr.png" logolink="" favicon="favicon-ispmgr.ico" localdir="default/">
        <metadata name="emaildomain.edit" type="form" mgr="ispmgr" decorated="yes">
          <form>
            <field name="name">
              <input type="text" name="name" required="yes" check="domain" convert="punycode" maxlength="255"/>
            </field>
     	// Поля формы
            <buttons>
              <button name="ok" type="ok"/>
              <button name="cancel" type="cancel"/>
            </buttons>
          </form>
        </metadata>
        <messages name="emaildomain.edit" checked="cba82687e7756e2c0195c88d4180f5d5">
          <msg name="currentmonth">текущий месяц</msg>
          //Локализация
        </messages>
        <doc lang="ru" func="emaildomain.edit" binary="/ispmgr" host="https://172.31.240.175:1500" features="cba82687e7756e2c0195c88d4180f5d50" notify="0" theme="/manimg/orion/" css="main.css" logo="logo-ispmgr.png" logolink="" favicon="favicon-ispmgr.ico" localdir="default/">
          <slist name="owner">
            <val key="usr">usr</val>
          </slist>
          <slist name="defaction">
            <val msg="yes" key="error">Сообщение об ошибке</val>
          </slist>
          <slist name="ipsrc">
            <val msg="yes" key="auto">получить автоматически</val>
          </slist>
          <name/>
          <avcheck>off</avcheck>
          <owner>usr</owner>
          <ipsrc>auto</ipsrc>
        </doc>
        <id>test.email</id>
        <ok/>
        <tparams>
          <clicked_button>ok</clicked_button>
        </tparams>
      </doc>
    </answer>
    <localmacro>
      <macros name="mpre_HostIP" field="ipsrc">auto</macros>
    </localmacro>

Доработки (о чем мы не подумали заранее)


Ожидание


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

Дозапись и редактирование шагов теста


Наверное, все помнят времена печатных машинок: одна ошибка — и приходится перепечатывать всю страницу.



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

Макросы для переменных


При выполнении тестов значения в передаваемых и проверяемых параметрах менялись в зависимости от сервера, на котором они были запущены. Пример таких данных — IP-адреса. Узнать их на этапе записи теста невозможно, поэтому я добавил систему макросов. Это позволило создавать тесты, не так жёстко привязанные к окружению. Недостаток решения в том, что после записи макросы надо указывать вручную.

Ещё одна проблема, существенно усложняющая работу с магнитофоном, — это использование не нативных ключей. Мы заметили её не сразу, так как тестировали магнитофон на ISPmanager, который использует нативные идентификаторы. Но в некоторых других панелях запись идентифицируется по уникальному ID. Поэтому пришлось научить магнитофон не только получать идентификатор после создания записи или объекта (так как ID может меняться от запуска к запуску), но и подставлять его во все последующие запросы.

Поддержка формата JUnit


Созданные магнитофоном тесты запускаются автоматически в среде непрерывной интеграции Jenkins. После выполнения тестов создаётся xml-файл, содержащий данные в формате JUnit. Чтобы файл корректно формировался, было введено ограничение на именование тестов. Например, тест User.Create.xml попадал в testsuite с именем User и, следовательно, testcase у него был Create. В случае ошибки к нему добавлялся дочерний узел failure с полным описанием ошибки.

Метрики


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

Хранилище тестов


Хранилище тестов в первую очередь решает проблему переноса готовых тестов на другие серверы. Ещё оно пригождается, когда тесты пишут несколько тестировщиков. Небольшая панель для хранения тестов Storage была разработана и развёрнута также на базе нашего COREmanager. В магнитофоне есть модуль синхронизации тестов с хранилищем. При написании нового теста или выгрузке тестов из хранилища они автоматически становятся недоступными к загрузке в хранилище, чтобы не возникло путаницы. После изменения теста у него повышается номер ревизии, чтобы в хранилище загружались только тесты после изменений.

Трудности (ну а куда же без них)


Использование магнитофона показало, что не все функции API следовали нашим же внутренним рекомендациям. В частности, не все функции возвращали идентификатор записи после её создания. Приходилось возвращаться в том числе к работающему коду и приводить его в соответствие с требованиями.

Ещё одной проблемой стали deadlock. Панель подразумевает выполнение некоторых критических действий в монопольном режиме. И магнитофон, являясь частью той же панели, вызывая такие функции приводил к зависанию всей системы. Это удалось определить только с помощью GDB (никто не вспомнил об этой особенности). К сожалению, не обошлось без костылей, потому что было принято решение при запуске тестов магнитофона выполнять эти функции в многопоточном режиме. Теоретически можно было оформить магнитофон не модулем, а отдельной панелью. Но мы не пробовали.

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

Заключение


Создание магнитофона помогло добиться повышения качества тестируемых продуктов. Сэкономлено время и ресурсы на обучение тестировщиков. Попутно магнитофон позволил провести review нашего API на соответствие внутренним рекомендациям, и, следовательно, сделать API чуточку более «логичным».

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


  1. ggo
    23.08.2018 09:18

    Насколько я понимаю это аналоги betamax и vcr только для ui?


    1. immaculate
      23.08.2018 09:22

      Нет, это явно не аналоги betamax и vcr. Я использую vcrpy в своем проекте для того, чтобы тесты не лезли каждый раз на реальный сервер, вместо этого им подставляются закэшированные vcrpy результаты.


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


      В двух словах: «Мы создали клевый инструмент для тестирования. Всё».


    1. vburov Автор
      23.08.2018 09:31

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


      1. ggo
        23.08.2018 09:37

        Понятно, в обратную сторону.


  1. shukshinivan
    23.08.2018 14:58

    Хм, интересная идея. Следующий шаг — это создание автотестов автоматически из поведения пользователей? :)


  1. shockable
    23.08.2018 22:59
    +1

    Record/playback testing?
    Вроде как давно не относится к рекомендуемым подходам.


  1. lxsmkv
    24.08.2018 03:08

    Классный велосипед с магнитофоном.
    vburov Что вам пришлость сделать чтобы ваша система заработала на дженкинсе? Как вы научили дженкинс выполнять тесты и производить junit xml?


    1. vburov Автор
      24.08.2018 06:15
      +1

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

      execute /usr/local/mgr5/sbin/mgrctl -m ispmgr test.synchronize 2>&1 | ( grep ERROR || exit 0 && exit 1 ) >> /root/ispmgr-int-player.log 2>&1
      /usr/local/mgr5/sbin/dotest --branch master --junit -o ispmgr-int-player.xml /usr/local/mgr5/var/ispmgr.test/ >> /root/ispmgr-int-player.log 2>&1
      File successfully received /usr/local/mgr5/ispmgr-int-player.xml


      1. lxsmkv
        24.08.2018 22:01

        vburov Простите если вопрос покажется наивным.
        Т.е. Ваша самописная система исполнения тестов (dotest --branch master --junit -o ispmgr-int-player.xml ) производит Junit XML? как вы ее этому научили?

        У нас ситуация такая, что есть лог после теста но нет Junit XML и не будет. Лог содержит классификаторы так что парсится он вполне, но нужно как-то создать валидный Junit XML. Потому что с ним-то уже много чего интересного можно делать, например в тот же Allure пихать.

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

        file.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?><testsuites  ....")
        валидный XML файл? Или как-то по другому?