Привет, Хабр!

За последние несколько лет работы с командами тестировщиков ПО в России и США мне довелось столкнуться с различными подходами к организации тестирования, разнообразными паттернами построения тестовых сценариев и разработки автоматических тестов. При этом нередко случалось, что приходя на проект и анализируя имеющуюся тестовую базу выяснялось, что существующие автотесты нуждались в серьезной доработке или вовсе переработке в целях обеспечения их надежности и сокращения времени на их выполнение. Преимущественно это касалось этапа сквозного (e2e) тестирования, и по моим наблюдениям очень часто причиной тому было не столько незнание тестировщиками встроенных команд тестового фреймворка, сколько неумение применить в тестах базовые возможности языка программирования.

Это побудило меня написать пару статей, в которых я хотел бы поделиться с вами некоторыми наработками по оптимизации автотестов Cypress, основываясь на простых и в основном известных возможностях JavaScript. Изначально статьи были опубликованы на английском языке в моем блоге "Testing with Cypress" на Medium.

Итак, почему же Cypress?

Cypress — это современный фреймворк сквозного (e2e) тестирования на основе JavaScript, разработанный для автоматизации веб-тестирования путем запуска тестов непосредственно в браузере. Cypress стал популярным инструментом веб-приложений благодаря ряду отличительных преимуществ, таких как удобный интерфейс, быстрота выполнения тестов, удобство отладки, простота написания тестов и т.д.

Те, кто уже имел какой-либо опыт работы с этим тестовым фреймворком, наверняка знают о его преимуществах, позволяющих обеспечить покрытие проектов качественными и надежными автотестами. Особо стоит отметить то, что Cypress имеет хорошо проработанную документацию c полезными рекомендациями для начинающих, которая постоянно совершенствуется, а также обширное комьюнити пользователей. Однако несмотря на удобство и сравнительную простоту для быстрого старта, все же не стоит забывать, что говоря про тесты Cypress в первую очередь мы имеем ввиду код. В этой связи для эффективной работы с Cypress от тестировщика требуется не только понимание тестирования ПО как такового и основ программирования, но и более-менее уверенное владение JavaScript/TypeScript.

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

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

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

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

1. Перебор набора/списка элементов

Циклы могут быть полезны в ходе перебора внутри набора элементов или списка объектов для выполнения повторяющихся задач. Например, если на веб-странице есть несколько элементов типа checkbox, то цикл может быть использован для выбора или снятия выбора с каждого из них. Цикл может быть контролируемым, устанавливая начальные и конечные значения. Использование циклов в подобном сценарии гарантирует, что все элементы или объекты будут перебраны при необходимости.

В данном примере мы используем метод Array.from() для преобразования значения, возвращаемого командой Cypress cy.get(), в стандартный массив. Затем мы используем цикл forEach(), чтобы перебрать каждый элемент и далее выполнить некоторое действие.

2. Повторение теста с разными входными данными или ожидаемыми результатами

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

В этом примере мы используем цикл for…of с деструктуризацией для повторения теста с разными входными данными и ожидаемыми результатами. Мы определяем массив объектов, каждый из которых имеет свойства input и expectedoutput, и используем цикл для перебора каждого объекта в ходе выполнения теста.

3. Навигация по нескольким страницам  

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

В приведенном примере показано, как использовать цикл forEach для навигации по нескольким страницам. Код создает массив URL-адресов страниц, а затем использует цикл forEach для посещения каждого URL-адреса и проверки того, что страница загрузилась корректно с помощью Cypress метода should().

4. Тестирование нескольких учетных записей или ролей пользователя  

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

Смысл использования цикла for...of в приведенном тесте Cypress заключается в том, чтобы перебрать массив пользователей и выполнить один и тот же тестовый сценарий с разными наборами данных. В данном случае, сценарий тестирования включает вход в систему с конкретными учетными данными пользователя, выполнение действий, специфичных для их роли, и выход из системы. Это экономит время и усилия при написании отдельных тестов для каждой пользовательской учетной записи или роли.

5. Запуск одного и того же теста с различными конфигурациями  

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


Использование цикла for…of в этом тесте позволяет оптимизировать выполнение теста, уменьшив повторение кода. Перебирая config массив и используя данные конфигурации для настройки тестовой среды, мы можем избежать повторения одного и того же кода при написании тестов для каждой конфигурации.

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

6. Динамическая генерация тестовых данных  

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

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

7. Итерация по набору тестовых шагов  

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

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

Внутри цикла тест проверяет свойство action для каждого шага, чтобы определить, какое действие должно быть выполнено. В зависимости от значения свойства, тест будет использовать различную Cypress команду для выполнения действия. Например, если действие - type, тест будет использовать команду cy.get().click(), чтобы имитировать нажатие кнопки.

8. Тестирование одной и той же функциональности или поведения в разных окружениях

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

Так, в приведенном примере внутри цикла forEach устанавливается размер области просмотра с помощью команды cy.viewport() с изменяемыми параметрами ширины и высоты. Использование цикла в этом случае позволяет запустить тест несколько раз с разными конфигурациями, не дублируя код теста. Это делает тест более эффективным и удобным для изменения, особенно когда требуется протестировать множество разных окружений.

Заключение

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

Спасибо за внимание и удачного тестирования!

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


  1. yarkov
    23.04.2023 06:19

    Не думал что такие очевидные вещи ещё и объяснять надо )


    1. alex_sanzh Автор
      23.04.2023 06:19

      Часто очевидные для разработчиков вещи не столь очевидны для тестировщиков) Благодарю за комментарий


  1. Desprit
    23.04.2023 06:19
    +1

    Отказались от Cypress в пользу Playwright, скорость и стабильность значительно выше у последнего!


    1. alex_sanzh Автор
      23.04.2023 06:19

      Благодарю за комментарий


  1. amakhrov
    23.04.2023 06:19

    Пункт 7 - итоговый код гораздо менее понятен, чем просто последовательный вызов методов Cypress.

    Пункт 9 - вообще какой-то лютый антипаттерн. Все запросы (queries) Cypress под капотом повторяются до успешного результата или таймаута. Непонятно, какую проблему решает тут цикл.

    Остальные пункты - по сути вариации одной и той же идеи - параметризированые тесты.

    в целях обеспечения их надежности и сокращения времени на их выполнение

    Ни надежность, ни время выполнения в статье так и не раскрыты остались.


    1. alex_sanzh Автор
      23.04.2023 06:19

      Пункт 7: что означает в данном контексте "просто последовательный вызов методов"? при заполнении формы состоящей из множества полей и других элементов намного проще имея например объекты с селекторами элементов и тестовых данных пройтись циклом используя внутри switch/if-else, чем прописывать последовательно методы для каждого элемента при этом повторяя одни и те же методы. это не сократит время на обработку формы, зато очевидно намного сократит код.

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

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

      Сокращение количества тестов за счет использования циклов очевидно сокращает общее время на прогон конкретного тест-сьюта (особенно с учетом уменьшения количества запуска before/after conditions). В отношении надежности соглашусь, что прямая связь неочевидна, в то же время сокращение кода при оптимизации позволяет предотвратить ошибки в случае дублирования кода в одинаковых по своей сути тестах, и ускорить обработку ошибок в случае ненадежности тестов.

      В целом благодарю за замечания.


      1. amakhrov
        23.04.2023 06:19

        просто последовательный вызов методов

        cy.get('#input1').type('some value')
        cy.get('#button1').click()
        cy.get('#input2').type('another value')
        cy.get('#checkbox3').click()

        По-моему, поддерживать такой тест гораздо проще, чем набор if/else

        Сокращение количества тестов за счет использования циклов очевидно сокращает общее время

        В ваших примерах количество тестов не сократилось.

        for (let user of users) {
          it(`test for user ${user.name}`, () => /* ... run assertions */)
        }

        в этом коде все так же 3 теста, как было бы и без цикла.


        1. alex_sanzh Автор
          23.04.2023 06:19

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


          1. amakhrov
            23.04.2023 06:19

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


            1. alex_sanzh Автор
              23.04.2023 06:19

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

               const fillForm = (selector, dataTest) => {
                          for (let key in dataTest) {
                              switch (key) {
                                  case "element1":
                                  ...
                                  case "elementN": {
                                      clickOnEl(selector[key + 'Button']);
                                      clickRandomFromList(selector[key + 'List']);
                                      break;
                                  }
                                   case "elementN+1":
                                    ...
                                   case "elementN+M":
                                    {
                                      clickOnEl(selector[key + 'Button']);
                                      clickOnDropdown(selector[key + 'List']);
                                      break;
                                  }
                                  default: {
                                      inputText(selector[key], dataTest[key]);
                                  }
                              }
                          }

              в подобном случае значит-го кол-ва элементов намного проще сгруппировать идентичные действия для различных элементов таким образом, для прочих - действие по дефолту (в примере - ввод текста для различных полей), и заполнять форму проходясь циклом по объектам селекторов/тестовых данных, чем прописывать для каждого элемента повторяющийся набор действий растягивая функцию на несколько десятков строк кода.

              в статье описан лишь принцип на самом простейшем примере.


      1. amakhrov
        23.04.2023 06:19

        оглашусь, отработка случаев, касающихся ожидания появления элемента, таким способом не есть best practice.

        Нет. Это не просто "не best practice". Этот цикл принципиально не делает то, что, согласно статье, он должен бы делать. Он не увеличивает таймаут в MAX_RETRIES раз, он не проверяет наличие MAX_RETRIES последовательных спиннеров. Он делает тест менее надежным - в зависимости от логики страницы он может иногда падать из-за того, что ассерты в Cypress асинхронные.


        1. alex_sanzh Автор
          23.04.2023 06:19

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


  1. alex_sanzh Автор
    23.04.2023 06:19

    .