Переводы

End to end тестирование с Jest, Puppeteer и Faker

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.

Перевод — Юлия Аюбова.

Исходный код

Макс

Recent Posts

Elm #2: Загрузка и отображение json

Сегодня будем использовать parcel и IntelliJ IDEA Community Edition. Все инструменты бесплатные. Инициализация elm проекта…

5 лет ago

Elm для React/Redux разработчиков (Elm #1: Знакомимся с Elm)

На данном вебинаре мы знакомились с языком Elm проводя параллели между Elm и Redux, поэтому…

5 лет ago

Масштабируем Elm-приложение (конспект)

Richard Feldman рассказывает как масштабировать Elm приложение без боли. Показаны техники: extended records, подход narrow…

5 лет ago

Elm видео, за ноябрь 2019

В данной заметке вы найдете конспект видео по Elm, которые я посмотрел в ноябре 2019.…

5 лет ago

Итоги 2019. Что учить фронтендеру в 2020?

Итоги года 2019 // Max Frontend Покажи мне свой гитхаб, и я скажу работал ли…

5 лет ago

Почему мне стоит изучать Elm?

Почему стоит изучать Elm? Потому что это интересный вызов, редкие (но вкусные) вакансии и хороший…

5 лет ago