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

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

Эта статья изначально появилась на Dev.to 🤖

Наконец-то вы запустили и запустили свое замечательное веб-приложение, и пришло время для тестирования…. Существует множество типов тестов, от Модульных тестов, где вы тестируете отдельные компоненты, составляющие ваше приложение, до Интеграционных тестов, где вы тестируете, как эти компоненты взаимодействуют друг с другом. В этой статье мы поговорим еще об одном типе тестов - End-To-End (e2e) тесты.

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



Для тестирования кода javascript одним из наиболее распространенных фреймворков для утверждений является Jest, который позволяет выполнять всевозможные сравнения ваших функций и кода, и даже тестирование React. компоненты". В частности, для выполнения тестов e2e на помощь приходит довольно новый инструмент Puppeteer. По сути это «парсер на основе Chromium. По репо:

«Puppeter - это библиотека Node, которая предоставляет высокоуровневый API для управления Chrome или Chromium по протоколу DevTools»

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

Вы можете найти репозитории обеих тестовых библиотек на github здесь:





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

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

🤘 Но, послушайте, вот вам отличный урок о том, как тестировать с Puppeteer + Jest, чтобы вы начали.

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

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

Ожидание, Ожидание, Ожидание… ⏰

Функция waitFor, установленная в Puppeteer, помогает нам справиться с асинхронностью. Поскольку эти функции возвращают Promises, обычно тесты выполняются с использованием функции async/await ES2017. Эти функции:

1) waitForNavigation

await page.waitForNavigation({waitUntil: "networkidle2"});

Каждый раз, когда действие пользователя приводит к переходу страницы к другому маршруту, нам иногда приходится немного подождать, прежде чем будет загружен весь контент. Для этого у нас есть функция waitForNavigation. Он принимает объект параметров, в котором вы можете установить timeout (в мс) или waitUntil, выполняется какое-то условие. Возможные значения waitUntil (согласно документации чудесного кукловода):

  • load (по умолчанию): считайте, что навигация завершена при возникновении события load.
  • domcontentloaded: считайте, что навигация завершена при возникновении события DOMContentLoaded.
  • networkidle0: считайте, что навигация завершена, если не более 0 сетевых подключений в течение как минимум 500 мс.
  • networkidle2: считайте, что навигация завершена, если установлено не более двух сетевых подключений в течение как минимум 500 мс.

Обычно вам нужно подождать, пока загрузится вся страница ({waitUntil: load}), но это не гарантирует, что все будет в рабочем состоянии. Что вы можете сделать, так это дождаться появления определенного элемента DOM, который гарантирует, что загружается вся страница. Вы можете сделать это с помощью следующей функции:

2) waitForSelector

Эта функция ожидает появления определенного селектора CSS, указывающего, что элемент, которому она соответствует, находится в DOM.

3) waitForFunction

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

Например, вы хотите дождаться изменения определенного сообщения. Тестирование будет выполняться путем получения сообщения с помощью функции evaluate() Puppeteer. Его первый параметр - это функция, которая оценивается в контексте браузера (как если бы вы вводили текст в консоли Chrome). Затем мы выполняем асинхронные операции, которые изменяют сообщение (нажатие кнопки или что-то еще 🖱🔨 ), а затем ожидая изменения сообщения.

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

Получение данных из селекторов после асинхронной операции

Как только мы обнаруживаем изменения, вызванные нашим асинхронным кодом, мы обычно хотим извлечь некоторые данные из нашего приложения, которые мы можем позже сравнить с ожидаемым визуальным результатом взаимодействия с пользователем. Мы делаем это с помощью evaluate(). Наиболее частые случаи, с которыми вы сталкиваетесь при извлечении данных:

- Проверка того, что элемент DOM появился

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

Ниже приведен пример проверки наличия в DOM сообщения с идентификатором post-id, где идентификатор - это число, которое мы знаем. Сначала мы сохраняем пост, дожидаемся сохранения поста, переходим в список / маршрут сохраненных постов и видим, что пост там.

Там мы можем наблюдать несколько вещей.

  1. Вышеупомянутое должно иметь уникальные идентификаторы для тестируемых элементов. Учитывая это, запрос к селектору намного проще, и нам не нужно выполнять вложенные запросы, которые получают элемент на основе его позиции в DOM (Эй, дайте мне первый элемент tr из первой строки этой таблицы 🙋🏼).
  2. Мы видим, как мы можем передавать аргументы функции оценки и использовать ее для интерполяции переменных в наши селекторы. Поскольку функция оценивается в другой области, вам необходимо привязать переменные из узла к этой новой области, и вы можете сделать это с помощью этого второго параметра.

- Проверка соответствия значений свойств (например, innerHTML, option…)

Теперь представьте, что вместо проверки наличия элемента в DOM вы на самом деле хотите проверить, действительно ли список сохраненных сообщений, отображаемых на странице, является сообщениями, которые вы сохранили. То есть вы хотите сравнить массив строк с именами сообщений, например ["post1,"post2"], с сохраненными сообщениями пользователя (которые вы можете знать заранее для тестового пользователя или получить из ответа сервера ).

Для этого вам нужно запросить все элементы заголовка сообщений и получить от них заданное свойство (так как это может быть их innerHTML, value, id…). После этого вам необходимо преобразовать эту информацию в сериализуемое значение (функция оценки может возвращать только сериализуемые значения или возвращает null, то есть всегда возвращать массивы , строки или логические значения, например, не HTMLElements…).

Пример выполнения этого тестирования:

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

-Утверждение того, что waitFor успешно

Еще одна вещь, которую вы можете сделать вместо evaluate(): если вы просто хотите подтвердить логический селектор или конкретное изменение DOM, просто назначьте вызов waitFor() переменной и проверьте, истинно ли оно. Обратной стороной этого метода является то, что вам придется установить предполагаемое время ожидания для функции , которое меньше, чем время ожидания Jest, установленное в начале. ⏳

Если этот таймаут превышен, тест завершится неудачно. Это требует, чтобы вы установили приблизительный тайм-аут, которого, по вашему мнению, достаточно для отображения элемента на вашей странице после того, как запрос сделан (Хм, да, я думаю, что около 3 секунд должно быть достаточно ... 🤔) .

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

Обратите внимание, что вы также должны привязать переменные к waitForFunction() в качестве третьего элемента, если вы укажете параметр waitForFunction как закрытие.

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

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

В следующем разделе мы увидим, как перехватить запросы и предоставить пользовательские данные, которые вам известны. Puppeteer предоставляет способ для этого, но если вы хотите еще двенадцать на подделку XMLHttpRequest и почти всех данных, которыми управляет ваш тест, вам следует взглянуть на Sinon.js 💖



Перехват запросов и подделка запросов с помощью Puppeteer

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

await page.setRequestInterceptionEnabled(true);

При этом мы можем настроить все запросы на перехват и имитацию данных ответа. Конечно, это требует знания структуры данных, возвращаемых серверной частью. Как правило, можно сохранить все объекты ответа с поддельными данными в отдельном классе, а затем, при перехвате запроса, запросить вызываемую конечную точку и вернуть соответствующие данные. Это можно сделать так, используя функцию page.on():

Отказ от ответственности: для API мы предполагаем несколько типичный формат https://api.com/v1/endpoint_name, поэтому синтаксический анализ для извлечения конечной точки специфичен для этого формата в целях демонстрации, вы можете обнаружить запрос, сделанный на основе других параметров, конечно, на ваш выбор 💪

Вы можете увидеть полную документацию по классу Request здесь

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

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

Получение данных для утверждения из Response

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

✍🏼 Примечание. page.on() методы запроса и ответа обычно помещаются в beforeAll() метод ваших тестов после объявления переменной страницы, поскольку они определяют глобальное поведение вашего теста.

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

Резюме

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

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

Есть вопросы ?! Иди сюда, начни внедрять тесты, как сумасшедший, и молиться, чтобы они прошли, удачного кодирования! 🙇🏻