End-to-end тестирование позволяет нам убедиться в том, что все компоненты нашего React-приложения работают ожидаемым образом в тех случаях, когда интеграционные и юнит тесты не помогают.
Puppeteer — это Node-библиотека от Google с высокоуровневым API для контроля Chromium через devtools protocol. C помощью нее можно открывать и запускать приложения и выполнять действия, указанные в тестах.
В этом посте я покажу, как использовать Puppeteer + Jest для запуска разных типов тестов для простых React-приложений.
От редакции:
В коде автора было много ошибок, в статье они все исправлены. На всякий случай мы приготовили репозиторий с конечным результатом, в которой можно подсмотреть весь код. Один из тестов там не работает, так как это необходимо для демонстрации фичи со «скриншотами» о которой написано ниже.
Настройка проекта
Начнем с установки простого React-приложения. Установим такие зависимости, как Puppeteer и Faker.
Используем create-react-app
для сборки React-приложения и назовем его testing-app
.
Нам не нужно устанавливать Jest, так как он уже имеется в create-react-app. Если вы установите его еще раз, ваши тесты не будут работать из-за конфликта двух версий Jest.
Далее нам нужно обновить test
в package.json
для запуска Jest. Также нам нужно добавить скрипт debug
. Он установит переменную окружения Node для режима отладки и вызовет npm test
.
Используя Puppeteer, мы можем запускать наши тесты без пользовательского интерфейса или в браузере Chromium.
Это отличная возможность, поскольку это позволяет нам видеть представление страницы, DevTools и сетевые запросы, проверяемые тестами. Единственный недостаток в том, что сильно замедляется работа при непрерывной интеграции (CI).
Мы можем использовать переменные окружения, чтобы определить, запускать тесты headless (без пользовательского интерфейса) или нет. Я настрою мои тесты таким образом, что когда я захочу увидеть их исполнение, мне нужно будет запустить debug
скрипт. Иначе я запущу test
скрипт.
Теперь откройте файл App.test.js
в директории src
и замените существующий код на этот:
В первую очередь мы импортируем (require
) puppeteer. Затем мы описываем (describe
) первый тест, где мы делаем проверку на начальную загрузку страницы. Здесь я тестирую, содержит ли тэг h1
корректный текст.
Внутри описания нашего теста, нам нужно определить переменные browser
и page
. Они требуются для прохождения теста.
Метод launch
помогает нам передать конфигурацию в наш браузер и позволяет нам контролировать и тестировать наши приложения с разными настройками браузера. Мы даже можем менять настройки страницы браузера через установку опций эмуляции.
В первую очередь настроим наш браузер. Я создал функцию isDebugging
вверху файла. Мы можем вызвать эту функцию внутри метода запуска. Внутри этой фунции будет объект debugging_mode
, содержащий три свойства:
headless: false
— Если мы хотим запустить тесты без пользовательского интерфейса (true
) или в браузере Chromium (false
).slowMo: 250
— Замедляет действия Puppeteer на 250 милисекунд.devtools: true
— Должны ли быть открыты DevTools (true
) во время взаимодействия с приложением.
Фукнция isDebugging
будет возвращать объект debugging_mode
или пустой объект в зависимости от значения переменной окружения.
В файле package.json
мы создали скрипт debug
, который будет устанавливать значение переменной окружения debug
в true
. В нашем тесте функция isDebugging
будет возвращать настроенные опции браузера, которые зависят от этой переменной.
Далее мы частично установим настройки нашей страницы. Это делается внутри метода page.emulate
. Мы устанавливаем свойства width
height
объекта viewport
, и устанавливаем userAgent
пустой строкой.
page.emulate
— это очень полезный метод для нас, так как дает возможность запускать наши тесты под разными настройками браузера. Мы также можем копировать атрибуты различных страниц в page.emulate
.
Тестирование содержимого HTML с Puppeteer
(Список API методов — прим.переводчика)
Теперь мы готовы к написанию тестов для нашего React-приложения. В этом разделе я буду тестировать тэг <h1>
и навигацию для того, чтобы убедиться, что они работают корректно.
Откройте файл App.test.js
и внутри блока test
прямо ниже объявления page.emulate
напишите следующий код:
По сути мы говорим Puppeteer перейти по адресу http://localhost:3000/
. Puppeteer проверит класс App-title
. Этот класс представляет наш h1
тэг.
Метод $.eval
на самом деле вызывает document.querySelector
на любом объекте-странице.
Puppeteer находит элемент, который соответствует этому классу, он будет передан в колбэк-функцию e => e.innerHTML
. Здесь Puppeteer сможет извлечь <h1>
элемент и проверить, содержит ли он Welcome to React
.
Как только Puppeteer закончит с тестом, вызов browser.close
закроет браузер.
Откройте командную строку и запустите скрипт debug: yarn debug
или npm run debug
.
Если ваше приложение прошло тест, вы должны увидеть что-то вроде этого в консоли:
Далее идем в App.js
и создадим элемент nav
как здесь:
Учтите, что все <li>
элементы имеют одинаковый класс. Вернитесь в App.test.js
для написания теста навигационного меню.
Перед тем, как мы это сделаем, давайте отрефакторим написанный ранее код. Ниже функции isDebugging
определите две глобальные переменные browser
и page
. Теперь напишите новую функцию beforeAll
, как показано ниже
Ранее я у меня не было ничего для userAgent
. Поэтому я использовал setViewport
вместо beforeAll
. Теперь я могу убрать localhost
и browser.close
и использовать afterAll
. Затем, если приложение установлено в режиме отладки, я просто хочу закрыть этот браузер.
Теперь мы можем пойти дальше и написать тест навигационного меню. Внутри блока describe
создадим новый test
, как показано ниже:
Здесь, я в первую очередь ищу элемент с классом .navbar внутри $eval функции и затем с помощью тернарного оператора записываю true или false в переменную navbar если такой элемент существует.
Далее мне нужно получить список элементов. Как и ранее, я использую $eval
с классом nav-li
. Мы ожидаем (expect
), что navbar
будет true
и длина listItems
равна 4.
(внимание, у автора ошибка — нужно использовать класс nal-li
, прим.переводчика)
Вы могли заметить, что я использовал $$
на listItems
. Это быстрый способ собрать все элементы с переданным селектором. Поскольку eval
не используется вместе со знаками доллара, здесь не будет колбэка.
Запустите debug скрипт для того, чтобы увидеть, может ли код пройти оба теста.
Имитация действий пользователя
Давайте посмотрим, как мы можем протестировать представление формы, имитируя ввод с клавиатуры, клики мышью и события тачскрина. Это будет сделано с помощью случайных пользовательских данных, сгенерированных библиотекой Faker.
Создайте в папке src
файл Login.js
. Это просто форма с нашими четырьмя полями ввода и кнопкой подтверждения.
Также создайте файл Login.css
, как показано здесь.
Этот компонент выложен на github, так что вы можете скопировать его прямо в свой проект:
Когда пользователь кликает по кнопке Login
, приложение должно показать Success Message. Поэтому создадим файл SuccessMessage.js
в папке src
. Также добавьте файл SuccessMessage.css.
Далее импортируем эти файлы в наш App.js
.
Затем я добавлю состояние state
в класс App
. Также добавлю метод handleSubmit
, который отменит действия браузера по умолчанию (prevent default function) и поменяет значение complete
на true
.
Еще добавлю условный оператор в конце класса, который определяет, показывать Login
или SuccessMessage.
Запустите yarn start
, чтобы удостовериться, что ваше приложение работает правильно.
Теперь я буду использовать Puppeteer для написания End-to-End теста, чтобы убедиться, что этот функционал работает корректно. Перейдите в файл App.test.js
и импортируйте faker
. Я создам такой объект user
:
Faker очень полезен в тестировании, так как он генерирует различные данные каждый раз, когда мы запускаем тест.
Напишите новый test
внутри блока describe
для тестирования формы авторизации. Тест будет кликать по нашим полям и вводить что-то в них. Затем тест нажмет кнопку submit
и подождет сообщение success
. Также добавлю тайм-аут в этот test
.
Запустите скрипт debug
и посмотрите, как Puppeteer управляет тестом!
Устанавливаем куки внутри тестов
Теперь я хочу, чтобы приложение сохраняло куку на странице при любом подтверждении формы. В куке будет содержаться имя пользователя.
Ради простоты я собираюсь отрефакторить мой файл App.test.js
для открытия только одной страницы. Страница будет эмулировать iPhone 6.
Так как я хочу сохранять куку при подтверждении формы, мы добавим тест внутри контекста формы.
Напишите новый блок describe
для формы авторизации, скопируйте и вставьте тест для формы логина авторизации внутри него.
Я также переименую тест в fills out form and submits
. Создам новый блок теста, называемый sets firstName cookie
, который будет будет проверять, установлена ли firstNameCookie
.
Итого файл с тестом сейчас выглядит так:
Page.cookies
будет возвращать массив объектов для каждой куки документа. Я использовал метод массива find
, чтобы проверить, существует ли кука. Таким образом можно удостовериться, что приложение использует сгенерированное Faker firstName
.
Если вы сейчас запустите скрипт test
, вы увидите, что тест не проходит, потому что он возвращает значение undefined
. Давайте исправим это.
В файле App.js
добавьте свойство firstName
в объект state
. Задайте его пустой строкой.
В метод handleSubmit
добавьте:
Создайте новый метод handleInput
. Он будет срабатывать на каждом инпуте для обновления состояния.
Передайте этот метод в компонент Login
в качестве prop.
В Login.js
добавьте onChange={props.input}
в поле firstName
. Таким образом, при любом изменении пользователем поля firstName
React будет вызывать этот метод.
Теперь мне нужно, чтобы приложение сохраняло firstName
куку на странице, когда пользователь кликает на кнопку Login
. Запустите npm test
, чтобы увидеть, проходит ли приложение все тесты.
Что если приложению нужна определенная кука, чтобы исполнить какое-либо действие, и эта кука была установлена в предыдущих страницах авторизации? Для этого в тесте вы можете установить куку с помощью setCookie.
Пример:
Скриншоты с Puppeteer
Скриншоты могут помочь увидеть, как выглядели наши тесты, когда они провалились. Давайте посмотрим, как делать скриншоты с Puppeteer и анализировать наши тесты.
В файле App.test.js
в тесте nav loads correctly
добавьте условие для проверки того, что длина listItems
не равна 3. Если это так, Puppeteer должен сделать скриншот страницы и обновить условие теста, чтобы длина listItems
была равна 3 вместо 4.
Очевидно наш тест не будет пройден, потому что у нас 4 listItems
в нашем приложении. Запустите скрипт test
в терминале и убедитесь, что тест провален. В то же время вы найдете новый файл screenshot.png
в корневой директории приложении.
Вы также настроить параметры скриншота:
fullPage
— Еслиtrue
, Puppeteer сделает скриншот всей страницы.quality
— В диапазоне от 0 до 100 можно установить качество изображения.clip
— Принимает объект, задающий область страницы, которую нужно выделить для скриншота.
Вы также можете создать PDF страницы, применив page.pdf
вместо page.screenshot
. У него свои уникальные настройки.
scale
— Это число, определяющее рендеринг веб страницы. По умолчанию 1.format
— Определяет формат страницы. Если он установлен, то он приоритетнее любых других параметров настроек высоты и ширины. По умолчаниюletter
.margin
— Отступы страницы
Обработка запросов в тестах
Давайте посмотрим, как Puppeteer обрабатывает запросы в тестах. В файле App.js
я напишу асинхронный метод componentDidMount
. Этот метод будет получать данные из Pokemon API. Ответ на этот запрос будет в формате JSON. Я добавлю его в state.
Добавьте pokemon: {}
в объект state. Внутри компонента добавьте этот <h3>
тэг:
Если вы запустите приложение, вы увидите, что данные получены успешно.
(не сразу, сначала отобразиться «Something went wrong», так как Rajat не проверяет в процессе запрос или нет — прим.переводчика)
Используя Puppeteer, я могу написать задания для проверки содержимого нашего <h3>
при успешном запросе и также перехватить ответ и спровоцировать неуспешный вариант. Таким образом, я могу увидеть, как работает мое приложение в обоих случаях.
В первую очередь я сделаю так, чтобы Puppeteer отправил запрос, и перехвачу ответ. Если url включает слово «pokeapi», тогда Puppeteer должен спровоцировать перехват ответа. Если нет, то все должно пойти как есть.
Откройте файл App.test.js
и напишите следущий код в методе beforeAll
.
setRequestInterception
это флаг, разрешающий мне доступ к каждому запросу, сделанному страницей. Если запрос перехвачен, он может быть прерван с соответствующим кодом ошибки. Я могу вызвать сбой или просто перехватить запрос после проверки какой-либо логики.
Напишем новый test
под названием fails to fetch pokemon
. Этот тест будет проверять тэг h3
. Я получу содержимое HTML и удостоверюсь, что внутри текст Received Pokemon data!
.
Запустите скрипт debug
, чтобы увидеть <h3/>
. Вы заметите, что текст Something went wrong
остается таким же все время. Все наши тесты прошли, это означает, что мы успешно прервали Pokemon-запрос.
Отметьте, что перехватывая запросы, мы можем контролировать, какие посылаются заголовки, какие возвращаются коды ошибок, и возвращать кастомные тела ответов.
Оригинал — Testing your React App with Puppeteer and Jest.
Перевод — Юлия Аюбова.