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


Проблема: тратим много времени на автоматизированное тестирование

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

Изначально прохождение 2000 E2E автотестов занимало у нас 16 часов или 2 рабочих дня. В среднем происходило 8 запусков на одну публикацию, а это значит мы тратили на один автотест 2 часа. 

Мы задались целью сократить этот показатель на 50%.

Решение: ускорить время прохождения автотестов 

В интернете можно найти разные советы по ускорению тестов, 70% из них относятся к «уберите thread.sleep», «уберите зависимости между тестами», «замените ui тесты тестами api» и, наконец, «давайте начнем mock’ать». Такие рекомендации не всегда работают для больших проектов, которые хотят тестировать всё приложение, а не отдельные его части. 

Мы решили пойти своим путём. Команда проанализировала текущие запуски и выделила основные направления для будущих работ. А именно: 

  1. Оптимизировать тестовый фреймворк

  2. Увеличить стабильность автотестов

  3. Оптимизировать очередность автотестов в тестовом запуске

  4. Увеличить параллелизации автотестов

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

Этап 1. Оптимизация тестового фреймворка

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

В нашей команде автоматизации сформировалась высокая культура программирования. Мы не используем thread.sleep в коде, только явные ожидания. Наши тесты не зависят от общих ресурсов: один тест не ждет пока другой освободит общий ресурс.

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

  • Использовать только явные ожидания;

  • Не использовать зависимости;

  • Подготавливать данные перед тестом и использовать общие ресурсы, которые не влияют на прохождение тестов;

  • Максимально заменять UI-шаги API-вызовами в тех случая, когда это необходимо. Например, проверять регистрацию через UI один раз, а в остальных случаях регистрировать через API.


Этап 2. Увеличение стабильности автотестов

E2E selenium тесты нестабильны, наши — не исключение. Если автотест упал один раз, то мы по умолчанию его перезапускаем. При повторном падении считаем, что есть проблема, тест больше не мучаем.

Каждый перезапуск увеличивает продолжительность всего тестового запуска, поэтому мы решили работать над стабильностью и выявлять причины такого поведения. Прикрутили Kibana, чтобы логировать каждый тест. В лог записываем  уникальный номер, время старта и завершения, окружение запуска и результат. Из Kibana получили статистику, где посмотрели стабильность автотестов. После отсортировали автотесты от наименее к наиболее стабильному. Каждую неделю мы собираем статистику, делаем выборку по всем тестам и получаем вероятность прохождения как passed_count/(passed_count + failed_count), сортируем по этому числу и начинаем разбираться в причинах. 

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

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


Этап 3. Оптимизация очередности автотестов в тестовом запуске

Мы не используем mock-объекты, а тестируем реальный бизнес-процесс, поэтому в запусках бывают тесты длительностью до 40 минут. С помощью Allure timeline мы заметили, что возникают ситуации, когда практически все тесты завершились, но остался один 30-минутный. Получается, что все ждут самого медленного, который еще одним из последних запустился. Встал вопрос, как продвинуть такой тест в самое начало.

Мы пишем автотесты на С# с помощью nunit framework. А он не предлагает удобного способа управлять очередностью запуска автотестов. Nunit предоставляет только order — атрибут, который работает только в рамках одного namespace, а сами namespace сортируются в алфавитном порядке. Поэтому самый простой способ подвинуть тест в самое начало — проставить букву А перед его названием в общем namespace.

Мы пошли дальше и сделали сортировку по строкам — названиям тестов. Тестам, которые длятся более 25 минут, мы присваиваем букву А вначале. Тестам от 20 до 25 минут — букву B, от 17 до 20 — букву С, от 14 до 17 — D и от 10 до 14 — букву E.

Чтобы определить, какие тесты пойдут без очереди, мы настроили билд в Jenkins. Он регулярно проверяет, начинаются ли с корректной буквы тесты длительностью от 15 минут. Если нет, то присылает уведомление команде автоматизации.

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


Этап 4. Увеличение параллелизации автотестов

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

Стандартно мы запускали автотесты в 30 потоков. На новые сервера добавили пару сотен виртуальных машин (максимально до 10 браузеров на каждой), увеличили количество потоков до 200 и стали ждать результатов. Мы удивились, что тестовый запуск при 200 потоках продолжался гораздо дольше, чем при 30.

Разбирая ситуацию, заметили, что проблема оказалась в API методах: то, что при 30 потоках выполнялось за 20 секунд, при 200 потоках занимало 4-6 минут. Также мы узнали о ServicePointManager.DefaultConnectionLimit — максимальном количестве параллельных коннекшенов для .net продукта. Когда увеличили их до 200, тесты побежали быстрее, но все равно не так быстро, как ожидалось. Появилось много flaky test.

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

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

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

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

Фактически сейчас мы имеем 7 Jenkins agents, на каждом из которых развернут свой собственных selenium hub c  2-мя executors.


Результаты

Мы решили поставленную задачу и ускорили автоматизированное тестирование. В результате мы смогли достичь следующих показателей:  

  • Прохождение одного автотеста сократилось с 2 часов до 1 часа;

  • Сократилось тестирование патча в среднем на 8 часов;

  • Увеличилась полнота автотестов;

  • Они стали стабильнее;

  • Код стал чище.

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


  1. alcochtivo
    18.11.2021 07:27
    -1

    Хорошая статья, больше похожая на отчёт) Если не секрет - почему используете виртуалки, а не какой-нибудь selenoid например?


    1. Dreamk Автор
      18.11.2021 12:36

      Мы пробовали перейти на selenoid/zalenium еще в 2017 году. Я тогда услышал про них на одной из конференций и вдохновился идеей.

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

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


  1. k_valiev
    18.11.2021 13:17

    В нашей команде автоматизации сформировалась высокая культура программирования. Мы не используем thread.sleep в коде, только явные ожидания

    Тут вы особо не выиграли. За wait-ерами все-равно спрятаны Thread.Sleep(к примеру https://github.com/SeleniumHQ/selenium/blob/trunk/dotnet/src/webdriver/Support/DefaultWait%7BT%7D.cs) А так у вас была возможность использовать await Task.Delay и освободить поток для других тестов, но тут придется очень сильно переделывать тесты. Если у вас тесты и тестируемый объект находятся на одной физической машине, возможно удастся еще чуть-чуть увеличить количество выполняемых тестов в единицу времени.

    Но чем ниже был уровень параллелизации, тем меньше было ложных падений.

    Я же правильно понимаю, что в системе с тестами только api и ui шаги. Если это так и с увеличением уровня параллелизации увеличивалось количество падений, то у вас проблема с атомарностью кейсов. Обычно в этом случае мы в командах начинали с выпиливания всех статических методов(за исключением чистых функций), после этого смотрели на бизнес сценарии и, либо выделяли соответствующие категории сценариев, либо делали тест атомарным. Скорее всего вам не надо было уменьшать количество потоков, а необходимо было править тесты.

    Мы пошли дальше и сделали сортировку по строкам — названиям тестов. Тестам, которые длятся более 25 минут, мы присваиваем букву А вначале. Тестам от 20 до 25 минут — букву B, от 17 до 20 — букву С, от 14 до 17 — D и от 10 до 14 — букву E.

    Не очень понял, чего вы хотели добиться? Есть тесты, которые:

    • могут исполняться параллельно;

    • должны запускаться последовательно относительно друг друга и могут запускаться параллельно относительно других тестов;

    • не могут исполняться одновременно с другими.

    Зачем вводить категории по продолжительности? По бизнес value - логично, по продолжительности не очень вижу логику. Они же все равно все должны пройти.


    1. Dreamk Автор
      18.11.2021 14:17

      Тут вы особо не выиграли. За wait-ерами все-равно спрятаны Thread.Sleep(к примеру https://github.com/SeleniumHQ/selenium/blob/trunk/dotnet/src/webdriver/Support/DefaultWait%7BT%7D.cs

      Понятно, что магии нет и все равно где то приходим к использования Thread.Sleep, но речь про совсем грязный код вида thread.sleep(TimeSpan.FromSeconds(30)), вместо явного ожидания появления элемента.

      Я же правильно понимаю, что в системе с тестами только api и ui шаги.

      В наших тестах есть все виды взаимодействия: api, ui, запросы к базе через драйверы и т.д. Атомарность кейсов соблюдена настолько, насколько это возможно. Но, как я писал ранее, мы решили придерживаться парадигмы автоматизации пользовательских кейсов, а не проверки кнопок и менюшек в вакууме. Мы пишем приемочные тесты, а они, по своему определению, не могут состоять из одного шага и проверки. Да, какие то шаги пересекаются, но они максимально упрощены.
      Еще раз отмечу, что при высоком уровне паралеллизации - большое число ложных срабатываний связано с максимальной загрузкой железа виртуалок из-за огромного количества запросов. Думаю, вам не нужно объяснять, что если cpu и диск под 100%, то о стабильности автотестов можно не мечтать.

      Зачем вводить категории по продолжительности? По бизнес value - логично, по продолжительности не очень вижу логику. Они же все равно все должны пройти.

      Идея была реорганизовать все кейсы таким образом, чтобы они запускались от наиболее долгого к наиболее быстрому. Когда речь про 1 поток, то разницы нет. А когда речь про многопоточность - появляются нюансы.
      Представьте ситуацию, когда у вас 10 тестов и 5 потоков. Из этих 10 тестов есть 3 теста, которые выполняются 15 минут каждый, остальные 7 тестов выполняются по 2 минуты.
      И в нашем случае получалось, что 2 из 3 долгих тестов попадали в один поток, что делало минимальное время прохождения запуска: 15+15 = 30 минут. Как бы быстро остальные тесты не бежали - все равно запуск будет ждать этот долгий поток с двумя тестами по 15 минут.
      После наших преобразований у нас в каждый поток попадает только 1 долгий тест, и получается что минимальная продолжительность тестового запуска уже 15+2=17 минут.


      1. k_valiev
        18.11.2021 15:31

        Думаю, вам не нужно объяснять, что если cpu и диск под 100%, то о стабильности автотестов можно не мечтать.

        Приложение, БД и тесты деплояться на один хост для запуска тестов? Просто есть подозрение, что cpu 100% при запуске в 30 потоков вам обеспечили тесты. На вряд ли приложение захлебнеться от 30 одновременно работающих пользователей. А вот если у вас синхронные тесты, то неосвобождение потока:

        • пока вы ждете элемент на странице(см. await Task.Delay(...));

        • пока вы ждете ответ от http сервера, сериализуете его;

        • пока исполняется скрипт в БД.

        Будет приводить к тому, что cpu будет захлебываться при 30+ потоках. Ваши тесты в 99% случаях просто ждут и при этом не освобождают ресурсы.

        Тестам, которые длятся более 25 минут, мы присваиваем букву А вначале. Тестам от 20 до 25 минут — букву B, от 17 до 20 — букву С, от 14 до 17 — D и от 10 до 14 — букву E.

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

        В наших тестах есть все виды взаимодействия: api, ui, запросы к базе через драйверы и т.д.

        Соответственно привести систему к любому состоянию - дело десятка секунд.

        • "тестирование патча в среднем на 8 часов". Я правильно понимаю, что придумали довольно хитрую систему с A, B ..., чтобы сэкономить максимум 25 минут из 8 часов? Но ведь для этого достаточно им приоритет поднять и все, плюс минус 15 минут относительно 8 часов, уже допустимая погрешность.


        1. Dreamk Автор
          18.11.2021 16:12

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

          Как я уже писал в самой статье мы сознательно отказались от использования моков и костылей в автотестах, и решили тестировать только пользовательские сценарии. В наших приложениях есть пользовательские сценарии, которые занимают столько времени. Другими словами - событие "B" происходит через 20 минут после события "А". Мы не пробрасываем сообщение в шину, не используем бэкдоры, а проверяем саму систему. Сама система это делает. Поэтому вопрос о плохом тестдизайне не стоит.

          Соответственно привести систему к любому состоянию - дело десятка секунд.

          К сожалению, нет. Система очень сложная, с кучей микросервисов (речь уже о тысячах). Подсунуть побыстрому что-то в базу не получается по многим причинам, в том числе и потому, что система живая и поддерживать такие костыли очень сложно. Запросы к базе у нас, по большей части, это SELECT, для каких-то очень важных точечных проверок.

           Я правильно понимаю, что придумали довольно хитрую систему с A, B ..., чтобы сэкономить максимум 25 минут из 8 часов?

           В среднем у нас происходит 8 запусков автотестов на одну публикацию. До всех преобразований 1 запуск длился около 2-х часов. Мы обратили внимание на эту проблему, когда заметили, что последний тест завершается через 15-20 минут после всех остальных. Сейчас в каждом запуске у нас не остаются длинные автотесты в конце, а значит тестировщик раньше увидит результат.
          Под поднятием приоритета, вы, наверное, имеете ввиду [Order()]. К сожалению, он действует только в рамках одного неймспейса. Организовать 2к+ тестов в одном неймспейсе чисто семантически не очень правильно.


          1. k_valiev
            18.11.2021 17:33

             "B" происходит через 20 минут после события "А"

            Интересный кейс, job scheduler так настроены? Или реально 20 минут процессинга? Видимо у меня просто личный опыт наложился, где я видел сценарии которые 20-25 минут кликают по экрану. Там были крайне сложные сценарии, где настраивались визарды, дублировались сложные логики, чтобы сделать тест атомарным, было очень большое количество проверяемых действий. Я даже не подумал, что у вас сценарий просто 20 минут ждет...

            Организовать 2к+ тестов в одном неймспейсе чисто семантически не очень правильно.

            Почему в один, вам главное значение order одинаковое проставить, количество namespace может быть разным.


            1. Dreamk Автор
              20.11.2021 17:18

              Интересный кейс, job scheduler так настроены? Или реально 20 минут процессинга? Видимо у меня просто личный опыт наложился, где я видел сценарии которые 20-25 минут кликают по экрану. Там были крайне сложные сценарии, где настраивались визарды, дублировались сложные логики, чтобы сделать тест атомарным, было очень большое количество проверяемых действий. Я даже не подумал, что у вас сценарий просто 20 минут ждет

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


              1. k_valiev
                21.11.2021 15:48

                Понял, спасибо за разъяснения! Классная статья!


  1. ivanych
    19.11.2021 21:38

    1. Что вы называете автотестом? Такое ощущение, что говоря "автотест" вы подразумеваете "все 2000 автотестов".

    2. Что значит "в среднем у нас происходит 8 запусков автотестов на одну публикацию"? Это 8 запусков одних и тех же тестов? Зачем?

    3. Что такое "публикация"? Это релиз новой версии? Как часто происходят "публикации"?


    1. Dreamk Автор
      20.11.2021 17:14

      Что вы называете автотестом? Такое ощущение, что говоря "автотест" вы подразумеваете "все 2000 автотестов".

      Автотест - это автоматизированный тестовый сценарий. Абстрактный пример такого сценария

      1. Регистрируем пользователей А и B

      2. Заходим в приложением пользователем А

      3. Отправляем заявку от пользователя А к пользователю В

      4. Заходим в систему пользователем В и проверяем, что он получил заявку от А

      Что значит "в среднем у нас происходит 8 запусков автотестов на одну публикацию"? Это 8 запусков одних и тех же тестов? Зачем?

      Процесс тестирования у нас проходит несколько этапов. Условно, это может быть Stage1, Stage2, Stage3. На каждом из этих этапов мы запускаем наши автотесты. По различным причинам, отсутствие багов при тестировании на stage1 является необходимым, но не достаточным условием для отсутствия багов на stage2 и так далее. Каждое из окружение имеет свои особенности, поэтому для гарантирования качества мы не можем пропускать эти этапы.

      Что такое "публикация"? Это релиз новой версии? Как часто происходят "публикации"?

      Публикация это появление какого то изменения на продакшене. Новая функциональность, например. Мы не готовим релиз(большое количество новых фич) в течении нескольких месяцев. Мы делаем частые публикации(которые, необязательно маленькие). Количество публикаций - переменная величина, но в среднем их больше 10 в неделю