Знакомство с фронтенд-тестированием. Часть третья. E2E-тестирование
30К открытий30К показов
Рассказывает Гил Тайяр, автор блога на Hackernoon
В прошлой части мы познакомились с юнит-тестированием: проверили основную логику приложения, содержащуюся в модуле calculator
, используя Mocha и тестовый стенд.
В этой части мы рассмотрим сквозное (E2E) тестирование: протестируем всё приложение целиком, причём сделаем это с точки зрения пользователя, по сути, автоматизируя все его действия.
В нашем случае приложение состоит только из фронтенда — бэкенда попросту нет, поэтому E2E-тестирование будет заключаться в открытии приложения в реальном браузере, выполнении набора вычислений и проверке валидности значения на экране.
Нужно ли проверять все перестановки, как мы делали это в юнит-тестах? Нет, ведь это уже проверено! В E2E-тестах мы проверяем работоспособность не отдельных юнитов, а всей системы сразу.
Сколько нужно E2E-тестов?
Первая причина, по которой таких тестов не должно быть много, — хорошо написанных интеграционных и юнит-тестов должно хватить. E2E-тесты должны проверить, что все элементы корректно связаны между собой.
Вторая причина — они медленные. Если их будет сотня, как юнит-тестов и интеграционных, то тестирование будет проходить очень долго.
Третья причина — непредсказуемое поведение E2E-тестов. О таком явлении есть пост в блоге Google, посвященном тестированию. В юнит-тестах не наблюдается такого нестабильного поведения. Они могут то проходить, то падать — причем без видимых изменений, исключительно из-за I/O. Можно ли убрать непредсказуемость? Нет, но можно свести её к минимуму.
Чтобы избавиться от непредсказуемости, делайте как можно меньше E2E-тестов. Пишите один E2E-тест на десять других, и лишь тогда, когда они действительно необходимы.
Пишем E2E-тесты
Перейдём к написанию E2E-тестов. Нам нужны две вещи: браузер и сервер для нашего фронтенд-кода.
Для E2E-тестирования, как и для юнит-тестирования, мы будем использовать Mocha. Мы настроим браузер и веб-сервер, используя функцию before
, и обнулим настройки при помощи функции after
. Эти функции запускаются до и после выполнения всех тестов и настраивают окружение, которое могут использовать тестовые функции. Узнать о том, как они работают, можно в документации Mocha.
Сперва взглянем на настройку веб-сервера.
Настройка веб-сервера в Mocha
Веб-сервер на Node? На ум сразу же приходит express, давайте посмотрим код:
В функции before
мы создаем express-приложение, указываем ему папку dist
и прописываем слушать порт 8080. В функции after
мы «убиваем» сервер.
Папка dist
— это то место, где мы храним наши JS-скрипты и куда копируем HTML- и CSS-файлы. Вы можете увидеть, что мы делаем это в сборочном скрипте npm
в package.json
:
Это значит, что для E2E-тестов нужно сначала выполнить npm run build
, а потом npm test
. Да, это неудобно. В случае юнит-тестов этого делать не нужно, так как они запускаются под Node и не требуют трансляции и сборки.
Для полноты картины давайте взглянем на webpack.config.js
, где описано, как Webpack должен делать сборку файлов:
Webpack будет читать наш app.js
и собирать все необходимые файлы в bundle.js
в папке dist
.
Папка dist
используется как в пользовательском окружении, так и в E2E-тестах. Это важно — запускать E2E-тесты нужно в средах, максимально похожих на «боевые».
Настройка браузера в Mocha
Наше приложение установлено на сервер — осталось лишь запустить для него браузер. Какую библиотеку мы будем использовать для автоматизации? Я обычно использую популярную selenium-webdriver
.
Для начала давайте посмотрим, как мы используем её, прежде чем начнём разбираться с настройками:
В функции before
мы готовим драйвер, а в after
— очищаем его. Подготовка драйвера будет запускать браузер, а очистка — закрывать его. Заметим, что настройка драйвера происходит асинхронно и мы можем использовать async/await
, чтобы сделать код красивее.
В тестовой функции мы открываем адрес http://localhost:8080
, снова используя await
, учитывая, что driver.get
— асинхронная функция.
Так как же выглядят prepareDriver
и cleanupDriver
?
Это сложная штука. И я должен кое-что признать: этот код был написан кровью (о, и он работает только в Unix-системах). Он был написан при помощи Google, Stack Overflow и документации webdriver и сильно модифицирован методом научного тыка. Но он работает!
Теоретически вы можете просто скопипастить код в свои тесты, не разбираясь в нём, но давайте заглянем в него на секунду.
Первые две строки подключают webdriver — драйвер для браузера. Принцип работы Selenium Webdriver заключается в наличии API (в модуле selenium-webdriver
, который мы импортируем в строке 1), который работает с любым браузером, и он полагается на драйверы браузера, чтобы… управлять различными браузерами. Драйвер, который я использовал, — chromedriver
, импортированный в строке 2.
Драйвер Chrome не нуждается в браузере на машине: он фактически устанавливает свой собственный исполняемый файл Chrome, когда вы выполняете npm install
. К сожалению, по некоторым причинам, которые я не могу понять, он не может найти его, и каталог chromedriver
должен быть добавлен в PATH
(это именно то, что не работает в Windows). Это мы делаем в строке 9. Мы также удаляем его из PATH
на этапе очистки, в строке 22.
Итак, мы настроили драйвер браузера. Теперь пришло время настроить (и вернуть) веб-драйвер, что мы и делаем в строках 11–15. А поскольку функция build
асинхронна и возвращает promise (промис), мы ждём её при помощи await
.
Почему мы делаем это в строках 11–15? Причины скрыты туманом опыта. Не стесняйтесь копипастить — никаких гарантий не прилагается, но я использовал этот код некоторое время, и проблем не возникало.
Приступим к тестам
Мы закончили настройку — пришло время взглянуть на код, который использует webdriver для управления браузером и тестирования нашего кода.
Разберём код по частям:
Пропустим установку, которую мы видели раньше, и перейдем к самой тестовой функции.
Код переходит к приложению и проверяет, что его название — «Calculator». Первую строку мы уже видели — мы открываем наше приложение с помощью драйвера. И не забываем дождаться окончания процесса.
Перейдём к строке 9. Здесь мы просим браузер вернуть нам заголовок (используем await
для ответа, потому что это асинхронно), а в строке 10 мы проверяем, что заголовок title
имеет корректное значение.
Так почему мы повторяем это, используя модуль promise-retry
? Причина очень важна, мы увидим, что и в остальной части теста браузер, когда мы попросим его что-то сделать, например, перейти по URL-адресу, сделает это, но асинхронно. Не позволяйте await
одурачить вас! Мы ждём, пока браузер скажет: «OK, я сделал это», — а не конца операции.
Поиск элементов
Дальше, к следующей части теста!
Теперь мы проверим, что первоначально display
равен 0
. Для этого найдем элемент, который содержит display
— в нашем случае это класс display
. Это мы делаем в строке номер 7 с помощью функции findelement
объекта класса webdriver
. Мы можем искать элементы с помощью методов By.id
, By.css
или других. Я обычно использую By.css
— он принимает селектор и очень гибок в использовании, хотя By.javascript
, вероятно, самый гибкий из них.
Как вы могли заметить, By
импортирован из selenium-webdriver
.
В строке 10 с помощью метода getText()
мы получаем содержимое элемента и проверяем его. Помните, что нужно дожидаться (await
) выполнения всех методов!
Пользовательский интерфейс
Настало время тестировать наше приложение — нажимать на цифры и операторы и проверять результат операций:
Сначала мы находим элементы, на которые хотим нажать, в строках 2–4. Затем нажимаем на них в строках 6–7. В нашем тесте получилось выражение "42*2="
. Затем мы повторяем процесс, пока не получим правильный результат, "84"
.
Выполнение всех тестов
Итак, у нас есть E2E-тесты и юнит-тесты, запустим их с помощью npm test
:
Всё отлично!4
Другие статьи серии
30К открытий30К показов