Трофей тестирования фронтенда
О тестировании можно говорить бесконечно долго, начиная с подходов из теоретической части (типа классов эквивалентности и эвристик), и заканчивая взаимодействием разработчиков с QA. Сегодня рассмотрим, что из себя представляет трофей тестирования и какие тесты нужно писать разработчикам в своих фронтенд проектах.
Пирамида тестирования #
Обычно когда речь заходит про тесты, говорят про пирамиду тестирования. По сути это способ визуального отображения, сколько в проекте должно быть тестов каждого вида (слоя):
В пирамиде выделяют 3 вида тестов:
- Юнит — тесты на отдельные модули, хелперы, компоненты в изоляции от всего остального приложения;
- Интеграционные — тесты на взаимодействие нескольких модулей, либо UI (но без запуска браузера и реальных сетевых запросов);
- Функциональные (или E2E, или UI) — тесты, когда запускается реальное окружение (браузер, тестовые стенды API) и тест прогоняется на странице в браузере, кликая по кнопкам, заполняя формы и работая с внешними сервисами/API.
Из этой пирамиды главное запомнить две вещи:
- Чем вид тестов находится ближе к вершине, тем тесты полезнее для бизнеса
- Чем ближе к основанию, тем тесты быстрее выполняются, их проще писать и поддерживать
Трофей тестирования #
Но что касается фронтенда, мне намного больше нравится визуализация от крутого инженера Kent C. Dodds, который представил так называемый трофей тестирования. В нем сильно изменились пропорции и добавился новый слой — «Статические тесты»:
Одна из основных мыслей, которая была заложена в трофей, звучит следующим образом:
Чем больше ваши тесты похожи на то, как пользователи пользуются приложением, тем больше гарантий они могут вам дать.
Если перефразировать, то самые полезные тесты это те, которые повторяют пользовательские сценарии, то, как пользователи пользуются тем или иным функционалом, а не проверка каких-нибудь краевых случаев внутреннего модуля. Но давайте по порядку.
Статические тесты #
Сегодня уже нет смысла доказывать, что TypeScript (или другой аналог со статической типизацией) приносит намного больше пользы в современные проекты со сложной логикой на клиенте, чем его отсутствие. Один из плюсов, TypeScript покрывает огромное количество кейсов, связанных с неверными типами, для которых раньше приходилось писать пачку unit-тестов. Вам очень повезло, если вы не писали такие тесты:
describe('sum', () => < it('должен как-то работать, если аргумент типа string', () =><>) it('должен как-то работать, если аргумент типа boolean', () => <>) it('должен как-то работать, если аргумент типа number', () => <>) it('должен как-то работать, если аргумент типа object', () => <>) // . >);
Сейчас же достаточно задавать валидный тип аргументам, и TypeScript уже сам позаботится, чтобы вы не вызвали у строки метод числа, типа ‘ops’.toFixed(2) .
Так же есть ESLint и IDE, которые позволяют отлавливать синтаксические ошибки. Пропустить скобку, кавычку или использовать переменную до момента ее объявления становится практически невозможно (конечно же возможно, но вы будете об этом уведомлены).
Все это Кент вынес в слой статических тестов:
Юнит тесты #
Следующий слой — это юнит тесты. Пишутся они на изолированные участки кода (функции, методы класса, хуки и т.д.) и выполняются на стороне NodeJS (на самом деле их можно и в браузере запустить, но обычно этого не делают).
Идеальнее всего они подходят для библиотек и модулей со сложной логикой или с большим количеством состояний.
Важно, чтобы unit-тестов не было много, и не нужно 100% покрытие.
Обычно для юнит тестов прикручивают инструменты, которые позволяют определить, а остались ли в коде логические ветки для которых не написаны тесты. И заставляют разработчиков писать тесты для всего подряд, а так же стремится к 100% покрытию. Не делайте так! В теории это звучит логично, но на практике у вам будет огромная куча тестов, которые ничего не проверяют. Бум. У меня был рабочий проект с 25000 юнит тестами, которые прогонялись за 10 минут (это долго для юнитов, если что); процентов 80% из них были бесполезными (проверяли какие-то синтетические случаи, и были написали только ради зеленой галки в CI).
Вам повезло, если вам ничего не говорит следующий комментарий кода в начале файла:
/* istanbul ignore file */
Это как раз таки одна из библиотек, которая отвечает за покрытие. И весь проект был пронизан подобными комментариями, что бы этот файл/функцию/строчку кода не учитывать в покрытии.
Но тут же отмечу, что это не касается всех проектов. Если вы пишете core-библиотеку или тулзу для разработчиков, которая предполагает использование в других проектах, то максимальное покрытие будет уместным.
Закрепим. Юнит тест подходят для библиотек, core- и сложной логики.
E2E тесты #
Эти тесты больше всего походят на то, как приложение используют пользователи. Почему же не писать только их? Проблем несколько:
- Для их написания нужно настроенное окружение — подготовленые стенды API, предоставляющие тестовые данные
- Такие тесты тяжело писать, отлаживать и поддерживать
- Они ну очень долгие
Представьте, что вы хотите протестировать регистрацию нового пользователя. Его нужно создать в базе данных, а что быть со следующим запуском теста (если пользователь уже будет в базе данных)? А что, если один тест запустят два разработчика на своих компьютерах? А если API перепускался и не работал пару минут во время прохождения тестов? Или во время прогона страница не доскролилась до нужного элемента? И это только вершина айсберга, подобных проблем много, и писать честные E2E довольно тяжело.
Поэтому, E2E должно быть еще меньше, чем юнитов, и писать их нужно только для самых критичных сценариев (авторизация, добавление товара в корзину и т.д.). Если у вас нет тестов других слоев, то большой соблазн начать писать именно такие тесты (так как они максимально покрывают пользовательские сценарии). Но остановитесь, лучше начать с интеграционных или юнит-тестов, а E2E оставить на лучшие времена.
Интеграционные #
Кто внимательно рассмотрел трофей тестирования, мог заметить, что интеграционным тестов выделено очень много места (на моей схеме это не так выделяется, извините, но в оригинале их площадь на схеме довольно большая).
Интеграционные тесты — это тесты, которые позволяют проверять взаимодействие между модулями приложения. В контексте бекенда или сервисной логики это значит тестирование работы модулей. А во фронтенде — тестирование пользовательских сценариев в UI. Так E2E тесты тоже тестируют пользовательские сценарии, чем интеграционные тесты лучше?
Особенность интеграционных тестов заключается в том, что они работают в изолированном окружении (все данные подготовлены заранее). В зависимости от инструментов, тесты так же могут изолироваться от реального запуска браузера, что делает их такими же быстрыми и стабильными, как юниты.
Еще одно преимущество, их можно писать не на все приложение, а на отдельные модули или виджеты. В контексте ныне полуряного микрофронтенда — на конкретный сервис.
Недостаточно? Еще один аргумент в их пользу — они пропагандируют подход black-box тестирования, когда мы не тестируем с разных сторон внутреннюю реализацию (как юниты), а тестуем внешний API модуля, как с ним будут взаимодействовать другие модули или пользователи. Тем самым при изменении внутренней реализации (сохранив внешний интерфейс использования), тесты не придется переписывать. Круто, да?
Самые частные кейсы для написания таких тестов:
- Клик по интерактивному элементу (кнопка)
- Открытие модальных окон
- Заполнение элементов формы и проверка клиентской валидации
- Отправка запроса на бекенд (сам запрос перехватывается и возвращаются замоканные данные) и отображение результата
Писать такие тесты не сложно, но придется сделать не мало подготовительных работ, о которых я расскажу в одном из следующих материалов (а пока материала нет, вы можете послушать часть из моего выступления на митапе, где я рассказываю о шагах, которые нужно сделать, чтобы написать интеграционный тест с использованием testing-library).
Я могу порекомендавать два вида библиотек для таких тестов:
- когда мы не хотим использовать браузер — однозначно testing-library. Очень крутое framework agnostic решение, которое позволяет писать тесты на наш UI
- когда хотим — cypress или playwright, которые так же подходят и для E2E тестов
Скриншотоные #
В интеграционных тестах есть несколько проблем. Одна из них заключается в том, что тесты не покрывают визуальную составляющую. Тест может успешно кликнуть на кнопку, которая скрыта другим блоком или css-стилем. А возможно вообще поехала верстка и UI виджет отображается криво.
Для таких случаев я выделяю еще один слой — скриншотные тесты (еще их часто называют скриншотные юнит-тесты). Делается скриншот UI-компонента (виджета или даже страницы), и все следующие прогоны сравнивают первый скриншот со сгенерированным в текущем прогоне.
Если у вас есть стенд компонентов (например, Storybook), то идеальнее всего травить скриншотные тесты именно на заготовленные стори. Причем есть довольно много уже готовых решений (например, loki), которые максимально упростят настройку инфраструктуры. У меня в текущем проекте несколько разных сторибуков, для всех них используем скриншотное тестирование (на момент написания статьи это пока в планах).
Вы можете сгенерировать скриншоты под разные брейкпоинты (например те, которые вы поддерживаете в проекте), разные темы (если у вас есть темная) и переключив компоненты в разные состояния. Шик.
Скриншот сторибука из одного моего проекта
Что по итогу? #
Мы прошлись по всем слоям (уровням) трофея и получили идеальный вариант для современного среднестатистический приложения:
Но не стоит забывать, что все очень сильно зависит от вашего проект (системная библиотека, долгоиграющий продукт или стартап), и в вашем случае идеальный трофей будет выглядеть совсем по другому.
Что я вам рекомендую сделать? Нарисуйте трофей вашего текущего проекта, устраивает ли он вас? А может какие-то слои слишком выделяются или их вообще нет? Ниже я нарисовал трофеи для последних трех рабочих проектов, на которых я работал:
А какой трофей тестирования у вас?
В одном (справа), мы очень строго покрываем все TypeScript-ом (в проекте у нас нет ни одного any и пару @ts-expect-error ), но совсем нет E2E тестов (из-за сложности подготовить тестовое окружение). Так же этот проект запускался в довольно сжатые сроки (совсем не до E2E тестов), поэтому в таких рамках — этот вариант трофея близок к идеальному.
В другом (средний) — очень много E2E и unit-тестов. Проект огромный, поддерживается сразу 6 командами, поэтому E2E помогают отловить многие ошибки интеграций. Но если быть честным, 80% тестов (что E2E, что юнитов) бесполезные, поэтому этот вариант для этого проекта не оправдал себя. Нужно добавлять интеграционные тесты и сильно сокращать текущие выпуклые слои.
И еще один проект (слева) имеет только статический слой. Как бы я хотел, чтобы это было осознанно (что это стартап, нужно срочно проверить гипотезу и нет времени на тесты). Но, к сожалению, это был очень важный проект — мобильное приложение на тысячи пользователей, на котором просто никто не писал тесты (включая меня). В этом случае трофей квадрат (если его можно так назвать) совсем не подходит к проекту. Нужно начинать писать тесты (хоть какие-нибудь), так как изменять функциональность на проекте очень сложно, много зависимых кейсов ломаются, а проверок для них нет.
А что получилось у вас?
Оставайтесь на связи
Чтобы не пропустить новые посты или анонсы проектов, которые я делаю, вы можете присоединиться к телеграм каналу . А так же оставаться на связи, задавать вопросы или просто вместе обсуждать различные инженерные темы.
Как тестировать современный фронтенд
Бывало, замечаешь в коде «нехороший» модуль или функцию и тут же тянутся руки их отрефакторить. Но как потом убедиться, что правки не сломали какой-нибудь сценарий в приложении или вовсе не положили продакшен? Этих ситуаций можно избежать, если в проекте есть тесты.
Меня зовут Александр Моргунов, я техлид в Самокате. Пишу на TypeScript, React, ReactNative. В разное время писал тесты для фронтенда, бэкенда и мобилок. В этом посте хочется поговорить о том, как можно тестировать современные фронтенд-приложения и какие подходы к тестированию сейчас актуальны.
Надеюсь, пост будет полезен для фронтендеров и тестировщиков, которые хотели бы свериться по дополнительным практикам для написания тестов. А бэкендеры смогут лишний раз посмотреть на схожесть и различия в тестировании для бэка и фронтенда.
Почему инженеры не пишут тесты?
Здесь у меня получился вот такой список.
Первая причина — неочевидность пользы тестов. Думаю каждый, кто когда-либо писал их, попадал в ситуацию, когда терялось само понимание, зачем нужен тест. Особенно если он не проверяет какие-то важные функции или вовсе пишется на какую-то малоиспользуемую функциональность.
Вторая причина – инфраструктура. Даже если инженер пересилил себя и написал тесты, то его ждёт второй барьер в виде часто сложной и долгой настройки инфраструктуры. Даже если она уже настроена, непонятно как же писать такие полезные тесты, чтобы они правда помогали приложению не падать.
Третьей причиной можно спокойно записывать сложность написания таких тестов.
Четвёртая причина, которую любят использовать некоторые менеджеры (персонажи вымышлены, совпадения случайны) — «у нас нет на тесты времени, мы продуктовыми фичами занимаемся».
Против всего этого есть два аргумента, которые на мой взгляд оправдывают написание тестов.
Первый – мы убеждаемся в том, что после каждого релиза наша основная функциональность не будет сломана и продолжит работать.
Второй – при внедрении новых фич, и особенно при рефакторинге, мы убеждаемся в том, что ничего не разломали в нашем приложении.
Дополнительно мы получаем документацию в виде кода. В том случае если есть фрагменты с legacy-кодом либо со сложной бизнес-логикой, по тестам можно понять, как вообще этот код предполагалось использовать, или какие кейсы предполагались к использованию.
Помимо этого, тесты помогают нам разбирать проблемы в архитектуре, но об этом чуть далее.
Основные подходы к написанию тестов для фронтенда
Здесь хочется кратко рассказать про пирамиду тестирования. Её придумали очень давно, ещё для бэкенда, и она делит всё тестирование на типы.
Обычно выделяют три типа:
- юнит-тесты – используются для тестирования функций, модулей, классов в изоляции от всего приложения;
- интеграционные тесты – тестируют взаимодействие между нашими модулями;
- end-to-end тесты – воздействуют на систему через её внешние интерфейсы и проверяют ожидаемую реакцию системы через эти же интерфейсы. Также пирамида показывает, сколько тестов должно быть в проекте по пропорции. Юнит-тестов должно быть больше всего, а end-to-end – меньше всего.
Чем ниже к основанию пирамиды располагаются тесты, тем они быстрее работают, тем стабильнее и быстрее пишутся. Но чем выше тесты к вершине пирамиды, тем они приносят больше бизнес-ценности.
Насчёт всего этого есть альтернативное мнение. Вот, например, что пишет крутой фронтендер Кент Си Доддс:
Чем ваши тесты больше похожи на то, как приложение используют, тем больше полезности и уверенности они вам принесут.
В контексте фронтенда Кент доработал обычную пирамиду тестирования и ввёл так называемый трофей тестирования.
В трофее тестирования у нас изменяется пропорция и добавляется дополнительный слой в самом основании – статические тесты. Он отвечает за тестирование ошибок типов, то есть за типизацию, и за различные синтаксические ошибки. Это достигается с помощью линтеров, Web IDE и какого-нибудь типизированного языка программирования, например, TypeScript. О последнем я хочу рассказать вам историю из жизни.
Время увлекательных историй
Кусок кода, который вы видите ниже, был написан пару лет назад для мобильного приложения и нужен для обновления пользовательского Access Token, чтобы пользоваться приложением.
refreshAccessToken() < try < /* . */ >catch (error) < const isNotInternetNetworkError = error.status; if (isNotInternetNetworkError) < this.data = undefined; >this.deps.softAppRestart(); > >
В нём не так интересно, как обновляется сам пользовательский токен, нежели что происходит, если токен мы не смогли обновить. В этом случае мы всегда перезагружаем приложение, и если у пользователя не было проблем с интернет-соединением, то мы удаляем пользовательские данные, то есть поле this.data мы приравниваем к undefined. Другими словами, мы разлогиниваем пользователя.
Для отсутствия соединения я представляю такие случаи, когда пользователь едет в метро или лифте, а приложение в этот момент пытается обновить Access Token. Чтобы не разлогинить пользователя, мы просто перезагружаем приложение.
Так что же произошло с фрагментом обновления пользовательского токена?
Когда случилась вся эта история, нас активно ддосили, ломались бэкенды, и для того, чтобы решить часть проблем, было решено удалить из базы данных так называемые Refresh Tokens, с помощью которых как раз и обновляются Access Tokens.
Это бы привело к тому, что всех пользователей просто разлогинило. Неприятно, конечно, но помогло бы решить текущие проблемы.
После того как все Refresh Tokens были удалены, приложение внезапно перестало работать у всех пользователей, и они стали видеть экран с заглушкой.
Мы попали в бесконечный цикл, в котором пользователи загружают приложение, мы пытаемся обновить Access Token, не можем его обновить, потому что Refresh Token уже в базе не хранится, и вместо того, чтобы пользователя разлогинить, мы просто перезагружаем приложение, и так по кругу.
Проблема крылась в том, что наш объект Error, который выкидывался в блоке Catch, был типа Any, иными словами, TypeScript неявно приводил его к этому типу.
В современных версиях и строгом режиме TypeScript не приводит Error к типу Any, а приводит к Unknown, и дальше можно будет уже самим привести нужный тип. Но у нас использовалась та версия, которая этого не делала.
Если бы мы просто добавили условие и сузили Error до нужной нам ошибки, Axios Error, Axios (НТТР-транспорт, который мы использовали в мобильном приложении), то нам TypeScript сразу подчеркнул ошибку и указал, что в переменной Error нет поля status.
catch (error) < if (isApiError(error)) < // error: AxiosError const isNetworkError = !error.status; // ^^^^^ // Property 'status' does not // exist on type 'AxiosError' if (!isNetworkError) < this.data = undefined; >> this.deps.softAppRestart(); >
Зайдя в исходники Axios, мы бы могли увидеть, что поле Status нужно было взять из error.response.status. Вот так приложение может сломаться из-за небольшой, но глупой ошибки.
catch (error) < if (isApiError(error)) < // error: AxiosError const isNetworkError = !error.response.status; // if (!isNetworkError) < this.data = undefined; >> this.deps.softAppRestart(); >
Казалось бы, причём тут тесты? А притом, что раньше на это все писали юнит-тесты, а сейчас с помощью строгой типизации подобные кейсы можно отловить прямо в коде. Нижний слой в трофее тестирования помогает нам решать и поймать огромное количество таких ошибок.
Структура теста
Файл с тестом я обычно создаю либо рядом с тестируемым модулем, либо создаю поддиректорию Tests, куда складываю все тесты.
Тест состоит из двух основных компонентов – блоков Describe и It. Дескрайбы могут быть вложенными, они описывают тестируемый модуль, тестируемую функцию, либо позволяют сгруппировать какие-то определённые связанные тест-кейсы.
А внутри блока It мы описываем сам тест. Я люблю так называть дескрайбы и иты, чтобы это можно было прочитать одним предложением.
Например, в нашем примере функция refreshAccessToken из service/Auth должна при сетевой ошибке очищать данные пользователя.
describe('service/Auth', () => < describe('refreshAccessToken', () => < it('должен при сетевой ошибке отчищать данные юзера', () =>< // Тело теста >); >); >);
Юнит-тесты принято писать по паттерну Triple A, когда мы группируем тело теста в три основные части.
it('должен при сетевой ошибке отчищать данные юзера', async () => < const authService = new AuthService(); // мок сетевого запроса await authService.refreshAccessToken(); expect(authService.data).toEqual(undefined); >);
- В Arrange мы инициализируем наши сервисы, подготавливаем моки.
- В Act – вызываем тестируемый метод.
- В Expect или Assert – проверяем данные, которые мы получили реально, сравниваем с данными, которые мы ожидали получить.
Когда какой тест писать
Юнит-тесты
Юнит-тесты выполняются на стороне NodeJS. Их можно писать, следуя практике Test Driven Development, когда вначале мы описываем тест-кейсы, а потом реализуем непосредственно сам код.
Я использую моменты из этой практики, например, когда нужно накидать интерфейс, либо какие-то ограниченные случаи, чтобы о них потом не забыть.
Юнит-тесты подходят как для библиотек, так и для модулей со сложной логикой — какими-то ветвлениями или с большим количеством состояний.
На одном из моих проектов у нас было написано порядка 25 тысяч юнит-тестов, и 90% из них были написаны только ради того, чтобы прошёл чек в GitHub и можно было дальше перевести задачу. По сути тесты были синтетические, на самые простые React-компоненты и функции писалось по несколько тестов.
Но как бы ни были хороши и просты юнит-тесты, как бы они быстро ни выполнялись, они не гарантируют того, что наше приложение будет вообще работать, так как они тестируют наши модули изолированно друг от друга.
Что касается мобильного приложения, у меня есть показательный пример, когда нужно писать юнит-тесты.
В мобильном приложении Самоката есть различные разделы, например, «Быстро» или «Бьюти», и у каждого раздела есть свой SLA доставки. Он отображается в нескольких местах приложения: в названии под витриной, в капсуле с корзиной и потом уже при заказе.
const getRoundMinutes = (value: number, options: Options) => < if (value >options.roundTo30.from) < const accuracy = 30; const mod = value % accuracy; if (mod < options.roundTo30.roundUpFromMod) < return Math.floor(value / accuracy) * accuracy; >else < return Math.ceil(value / accuracy) * accuracy; >> else if (value >= options.roundTo10.from) < const accuracy = 10; const mod = value % accuracy; if (mod < options.roundTo10.roundUpFromMod) < return Math.floor(value / accuracy) * accuracy; >else < return Math.ceil(value / accuracy) * accuracy; >> else if (value >= options.roundTo5.from) < // .
Выше представлен только фрагмент метода, который позволяет нам округлить и отформатировать для каждого места количество минут, то есть наш SLA, к нужному формату. Сразу можно понять, что это отличный кандидат для того, чтобы написать юнит-тесты.
Мы полностью покрыли его юнит-тестами, потому что при попытке что-то изменить либо доработать большая вероятность что-то сломать.
Для себя же я вывел правило: если я по фрагменту кода не могу за 1-2 минуты разобраться, что тут вообще происходит, из-за количества состояний, либо какой-нибудь сложной алгебры, то надо писать юнит-тесты.
Е2Е-тесты
Если Кент Си Доддс сказал, что нужно писать тесты, которые были бы максимально приближены к реальному использованию приложения, то давайте писать только одни Е2Е-тесты.
Напомню, что Е2Е-тесты требуют для себя браузер (там тесты выполняются) и настроенное окружение в виде подготовленных API и бэкенда, чтобы у нас тест реально туда ходил.
Из всего этого следует, что такие тесты тяжело писать, поддерживать и отлаживать.
Представьте кейс, в котором нам нужно протестировать регистрацию. Мы не можем регистрировать одновременно или друг за другом одного и того же пользователя и нам нужно придумывать, как мы эти тесты будем выполнять и, возможно, как-то чистить базу. Тестовые API периодически будут падать, а вместе с ними и тесты.
Поэтому Е2Е-тестов должно быть намного меньше, чем юнитов, и они должны быть написаны только для самых критичных пользовательских сценариев. Условно, это авторизация, регистрация, оплата, добавление товаров в корзину и так далее.
Приведу пример на основе одного из своих прошлых проектов. У нас было написано порядка 3000 Е2Е-тестов и выполнялся этот пак порядка 3-4 часов. Но самая большая проблема была даже не с самими тестами, а с падениями. В каждом прогоне падало около 5% тестов. Кажется, что это немного, но на самом деле это целых 150 тестов.
Релизный тестировщик мог целый день сидеть и разбирать, почему же эти 150 тестов упали. Часть тестов падала, потому что браузер мог лагануть, часть тестов – потому что API был недоступен, и только какая-то небольшая доля могла упасть, потому что и правда были проблемы. Ко всему этому релизы у нас были ежедневные, и тестировщик мог просто 40 часов в неделю отлаживать эти упавшие тесты. Для тестирования это жуткая боль.
Интеграционные тесты
Ранее вы могли заметить, что в трофее тестирования очень много места уделено интеграционным тестам, и это неспроста. Что это такое?
Это тесты, которые выполняются на среде NodeJS, и каждый из них выполняется в изолированном окружении. Если мы тестируем два-три модуля, мы используем их, а всё остальное окружение, внешние API, мы либо мокаем, либо подготавливаем для них стабы, и не используем.
Интеграционные тесты, помимо проверки различной бизнес-логики либо инфраструктуры, также могут тестировать пользовательские сценарии, как и Е2Е-тесты. Для них не требуется какого-то отдельно настроенного окружения и в отличие от юнит-тестов, они не тестируют одно и то же с одних и тех же сторон, но тестируют модуль в целом.
Я сделал сводную табличку по типам тестов и различным параметрам. Жирным выделено то, что выделяет тест на фоне других. Даже по этим параметрам можно понять, что интеграционные тесты выигрывают у тех же юнитов и Е2Е-тестов.
NodeJS
NodeJS
Для чего используются
библиотечный код и сложная ветвистая логика
пользовательские сценарии, взаимодействие модулей
критичные пользовательские сценарии
изолированные
изолированные
нужны отдельные API стенды
быстрые
быстрые
средняя
высокая
Отдельно хочется сказать про Black Box. Это такой подход к тестированию, который говорит о том, что нам не нужно знать, как работает наш модуль либо функция внутри – нам важно, что мы подаём на вход и что получаем на выходе.
Например, у нас есть функция по сложению элементов массива. На вход мы подаём массив, на выходе мы получаем сумму. И нам неважно, как функция устроена внутри, будь это просто обычный перебор массива и сложения, аккумуляция значений, либо это будет какое-нибудь параллельное вычисление на веб-воркерах, когда массив бьём и отправляем их асинхронно вычисляться. В данном подходе это неважно.
Почти во всех тестах я рекомендую использовать именно этот способ, потому что он обладает большим преимуществом по сравнению с White Box, когда мы завязаны на реализацию. Если мы как-то меняем внутренние API нашего модуля, нам не нужно переписывать тесты, потому что они завязаны именно на внешний API.
Стек для тестов
Jest
Сейчас самым популярным решением для запуска тестов является Jest. Это очень сложная и большая монорепа с кучей различных хелперов и инструментов для того, чтобы мы без проблем могли покрывать наш код тестами.
Я не скажу, что с Jest у вас не будет проблем (их будет много), но почти на все проблемы есть ответы и примерные варианты решения на Stack Overflow либо GitHub Issues – вы наверняка найдёте то, что искали. На Jest можно запускать юнит-тесты, интеграционные тесты и даже Е2Е-тесты.
testing-library
Я считаю, что каждый фронтенд-разработчик должен как минимум знать про эту библиотеку, даже если он её не использует. Она применяется для тестирования компонентов, и у неё есть очень крутая особенность. Она использует подход Black Box, и с помощью неё можно тестировать код на различных фреймворках, будь это Vue, React, Svetle, либо вообще это может быть какой-то ваш самописный движок. Также с помощью testing-library можно тестировать и свои собственные компоненты. Она не завязывается на внутреннюю реализацию и предоставляет единый API для всех решений, который мы можем использовать.
describe('', () => < async it('should display baz', () =>< render(); fireEvent.click(screen.getByText('Change to foo')) await waitFor(() => screen.getByRole('heading')) expect(screen.getByRole('heading')).toHaveTextContent('foo') >); >);
В этом фрагменте кода приведён пример теста с помощью testing-library. Мы монтируем в данном случае компонент, далее кликаем по какому-то элементу, ждём, пока у нас появится ещё один элемент и в конце проверяем, что в появившемся элементе содержится определённый текст.
Это уже пример даже не юнит-теста, а интеграционного UI-теста, когда мы по шагам проходим пользовательский сценарий и что-то проверяем.
Testing-library рендерит наши компоненты на стороне NodeJS (т.е на сервере) и для этого «под капотом» она использует библиотеку jsdom, которая предоставляет API браузера в NodeJS-среде.
У нас в NodeJS появляется глобальный объект Document, мы можем там внедрить какой-то HTML, либо загидрировать React-приложение и работать с ним (удалять ноды, искать ноды и тд).
Также в юнит-тестах и в интеграционных нам нужно мокать внешние запросы. Для этого можно использовать, например, mock service worker либо nock.
Для Е2Е-тестов, если вы решите их написать, или будете с ними заниматься, сейчас рекомендую cypress, очень крутое решение, которое позволяет писать простые и надёжные тесты.
Для генерации каких-то фейковых фикстур, например, банковских карт, емейлов — рекомендую решение faker.
Если вы хотите получить красивый отчёт, то есть библиотека jest-allure, которая позволяет по выполнению тестов сформировать HTML-страничку с их прогоном.
Как написать интеграционный тест
Переходим к самой интересной части. Если они такие полезные, как их правильно писать? Для этого я собрал небольшое приложение на React. По клику на кнопку пользователь получает случайную гифку с котиком. Я думаю, вы согласитесь, что если пользователь придёт и не получит котика, потому что там внедрится какой-то баг, это будет очень страшно, поэтому напишем интеграционный тест и обезопасим наших пользователей.
Перед тем как написать интеграционный тест, нам нужно понять, какие шаги в нём будут. В данном случае нам нужно смонтировать наш компонент. Будет отображаться только кнопка.
Вторым шагом – нажать на кнопку, третьим шагом – показать loader и ждать, пока у нас загрузится изображение, и в конце проверить то, что на изображении у нас отображается наш котик, то есть проверить source изображения, и убедиться, что там будет то, что мы заранее подготовили.
Возникает вопрос о том, что мы заранее подготовили. Где-то между шагами нам нужно замокать ответ от сервера, чтобы не отправлять реальный запрос в API.
Сам тест можно оформить по обычной схеме в виде дескрайбов и итов. Но в тело теста для удобства можно использовать самописный хелпер step, который позволит нам все эти шаги как-то структурировать и писать не общей кашей, а немного их разделить.
Заготовка для теста:
describe('feature/Сat', () => < it('должен при нажатии на кнопку загружать нового кота', async () => < await step('1. Монтируем компонент', () =><>); await step('2. Кликаем по кнопке "Give me a cat"', () => <>); await step('3. Ждем завершение загрузки', () => <>); await step('4. Проверяем появление гифки на странице', () => <>); >); >);
Также с помощью хелпера можно потом эти красивые шаги перенести в отчёт, чтобы там видеть информацию по каждому шагу, например, сколько каждый шаг выполнялся по времени.
Прежде, чем приступим к написанию самих этих шагов, разберём парочку моментов. Сначала я очень рекомендую создавать отдельный конфиг для Jest для интеграционных тестов, со своими настройками, своей маской для файлов и так далее.
// jest.config.js if (process.env.INTEGRATION_MODE) < module.exports.testMatch = ['**/*.intergration.spec.tsx']; module.exports.setupFiles.push('/tests/global.ts'); // . >
В простом кейсе можно не создавать отдельный конфиг, можно просто задать какую-то переменную окружения и уже в существующем конфиге подменять свои настройки. Выше я показал, что обычно для интеграционных тестов задаётся своя маска для файла, .integration.spec.tsx, чтобы их отделять от юнит-тестов.
Дальше нужно подготовить пейдж-объекты. Это такие объекты либо классы, которые позволяют нам инкапсулировать логику работы с нашими дом-узлами или компонентами в едином месте. Покажу это на примере.
const datePickerPO = < selector: '.date-picker', open: () =>*. */>, setDay: (day: number) =>*. */> , close: () =>*. */> , > // . datePickerPO.open(); datePickerPO.setDay(5); datePickerPO.close();
Например, у нас есть Datepicker, можем создать обычный объект, DatePickerPO, в нём указать selector, чтобы мы могли найти Datepicker, и методы для открытия попапа, для выбора необходимого дня и для закрытия. Потом в нашем тесте мы будем уже использовать не какие-то методы и селекторы для поиска, а непосредственно уже наш Page Object. Это позволяет сократить и код текста и избавиться от дублирования.
class LoginPagePO extends PagePO < get username () < return $('#username') >get password () < return $('#password') >get submitBtn () < return $('form button[type="submit"]') >get flash () < return $('#flash') >get headerLinks () < return $$('#header a') >async open () <> async submit () <> >
На самом деле нет определённого формата для описания пейдж-объектов, всё зависит от инструментов, которые вы используете. Например, Page Object может быть классом, который наследуется от какого-то базового пейдж-объекта, и там уже будет задаваться по своей форме.
В нашем же случае пейдж-обжет для приложения с котиком будет выглядеть следующим образом.
У нас есть какие-то методы, чтобы кликнуть по кнопке, метод GetLoader, чтобы получить наш Loader и проверить его, и есть метод, который позволяет ожидать, пока у нас Loader скроется, уйдёт из Dom.
fireEvent, getByTestId, queryByTestId, waitForElementToBeRemoved — места, которые предоставляет как раз testing-library для работы с нашим домом.
export const awesomeCatPO = < clickToButton() < fireEvent.click(getByTestId(document.body, 'button')); >, getLoader() < return queryByTestId(document.body, "loader"); >, async waitForLoaderHide() < await waitForElementToBeRemoved(() =>< return this.getLoader() >) >, // . >
Клик по кнопке можно заменить на Button Page Object, то есть, пейдж-объекты могут быть вложенные, и в идеале для каждого UI-компонента должен быть написан свой Page Object.
export const awesomeCatPO = < clickToButton() < fireEvent.click(getByTestId(document.body, 'button')); buttonPO.click(); >, getLoader() < return queryByTestId(document.body, "loader"); >, async waitForLoaderHide() < await waitForElementToBeRemoved(() =>< return this.getLoader() >) >, // . >
Далее нам нужен Helper для монтирования в Dom. Обычно компоненты не будут работать в изоляции от всего приложения, как бы нам этого ни хотелось. Для этого подготавливается какой-то универсальный Helper, который подготавливает глобальные объекты (например, Store), необходимые для рендеринга компонента, и используются потом уже в тестах.
function mountComponent(< Component, props, state >) < const store = mockStore(); return < component: render( > /> , ), store, >; >
Разработчики testing-library не предоставили из коробки функциональность, чтобы мы могли искать dom-элементы по CSS-классу либо по индификатору. По мнению разработчиков testing-library, пользователь не ищет элементы по CSS-классу, пользователь ищет элементы по плейсхолдерам, по тексту на кнопках и в тестах нужно это учитывать.
С одной стороны это правда, но с другой, практической стороны искать элементы по тексту/лейблу/плейсхолдеру довольно сложно во многих кейсах. Поэтому можно указывать data-testid атрибут у компонентов, с помощью которого потом можно будет искать элементы в тестах. В нашем случае мы задаём data-testid атрибут для контейнера, лоадера, кнопки и изображения.
В коде это выглядит примерно так.
return ( /> );
С одной стороны это немного засоряет наш код какой-то тестовой информацией, с другой стороны мы всегда понимаем, на какие элементы у нас завязаны тесты. Если мы что-то меняем, то это значит, что надо поменять и тест. Ещё один плюс — мы не завязываем наш UI именно на вёрстку, то есть после смены местами компонентов, либо изменения классов тест продолжит работать.
В заключение нам нужно замокать всё нужное и ненужное. Нам нужно замокать стили, картинки, внешние API-запросы, какие-то глобальные модули, так как у нас всё должно работать в изоляции. В принципе всё это можно сделать с помощью стандартных средств Jest.
Подытожим, что нужно сделать:
- настроить Jest,
- написать пейдж-объекты – это можно делать итерационно, если мы пишем тест для кнопки, то и Page Object пишем только для кнопки
- написать Helper для монтирования (итерационно)
- написать data-testid атрибуты (итерационно),
- замокать всё нужное и ненужное,
- подключить красивый вывод отчёта (по желанию).
Всё, давайте приступим к шагам. У нас есть уже заготовленный Helper Mount Component, с помощью него монтируем компонент.
await step('1. Монтируем компонент', () => < mountComponent(< Component: AwesomeCat, state:* . */> , >); >);
Второй шаг. У нас есть заготовленный Page Object, нам достаточно вызвать уже готовый метод Click to Button, то есть имитировать кнопку клик по кнопке.
await step('2. Кликаем по кнопке "Give me a cat"', () => < PO.clickToButton(); >);
Далее мы мокаем ответ от сервера, в данном случае это достаточно просто сделать с помощью даже встроенных средств Jest. На фрагменте показано, как мы подготавливаем фикстуру ответа от API, который нам предоставляет API.
jest.mock('axios'); function mockGiphyRequst() < axios.get.mockResolvedValue(* . */>); >
Третий шаг. Мы ждём завершения загрузки, но вначале проверяем, что Loader в принципе появился. Для этого мы используем наш кастомный Page Object и кастомный Matcher toBeInTheDocument, который предоставляет Jest, и из пейдж-объекта ждём, пока спиннер не скроется.
await step('3. Ждем завершения загрузки', async () => < expect(PO.getLoader()).toBeInTheDocument(); await PO.waitForLoaderHide(); >);
На четвёртом шаге мы проверяем то, что у нас появилось изображение, и проверяем поле Source – там мы должны были получить тот путь до изображения, который мы замокали заранее. Всё, мы написали полноценный интеграционный тест с пейдж-объектами, с data-testid атрибутами.
await step('4. Проверяем появление гифки на странице', () => < expect(PO.getImage()).toBeInTheDocument(); expect(PO.getImage()).toHaveAttribute('src', 'mocked.gif'); >);
Какие мы ещё получаем дополнительные бонусы от интеграционных тестов? Первое, что мы получаем по сравнению с Е2Е-тестами — очень дешёвую проверку пользовательских сценариев.
Также интеграционные тесты позволяют проверить модульность нашей системы, и если нам при написании какого-то теста приходится замокать всё подряд и использовать кучу модулей от приложений – соответственно, у нас с модульностью что-то не так, модули связаны и, возможно, стоит об этом задуматься.
И ещё один бонус – как и в Е2Е-тестах, шаги для тестов могут нам помогать писать QA-инженеры. В простой схеме можно просто прийти к тестировщику, попросить его накидать тест-кейсов, а можно построить полноценный процесс. Тимлид или бизнес ставит задачу, разработчик выполняет. В это время тестировщик пишет тест-кейсы для наших интеграционных тестов, и в конце в рамках отдельной задачки разработчик по этим написанным кейсам просто реализует наши тесты.
Проблемы интеграционных тестов
Первая проблема – это моки для внешних зависимостей. На реальных тестах у нас скорее всего будет не один запрос, а 2-4 и всё это мокать, подготавливать для этого фикстуры – довольно тяжело.
По своему опыту могу сказать, что появляется очень большой соблазн начать писать какую-то фабрику либо генератор для создания этих фикстур, но потом это всё настолько усложняется, что при написании теста правильно настроить генератор становится сложнее, чем написать сам тест.
Обычно я рекомендую использовать простые json-объекты, то есть самые примитивные фикстуры. Да, они будут немного дублироваться и при изменении API придётся их переделывать, но это сделать намного проще, чем возиться с генераторами.
Следующая проблема — сложность дебага. Так как у нас нет визуальной части, тесты выполняются в NodeJS, то при падении теста нам testing-library любезно предоставит HTML и сообщение об ошибке. На простом примере это не выглядит чем-то проблемным, но когда у нас компоненты сложные, мы получаем огромную портянку кода и ошибочку (кнопка не нажалась или элемент не появился), в этом довольно сложно разбираться.
Но есть хорошее решение jest-preview, которое локально позволит запустить браузер и прогнать наш интеграционный тест прямо там. Нужно будет немного помучаться с настройкой, потому что нам нужно полностью собрать приложение, с CSS и картинками, но это того стоит.
Ещё одна особенность интеграционных тестов – это отсутствие визуальной части. На NodeJS, например, на какой-нибудь форме регистрации кнопка с регистрацией может просто быть скрыта обычным CSS, либо перекрываться другим блоком.
Чтобы решить эту проблему, обычно вводят дополнительный слой тестирования – так называемые «скриншотные тесты». Они позволяют отрендерить компонент в браузере, возможно, в headless browser, сгенерировать скриншот и потом этот скриншот сравнивать с новыми полученными скриншотами.
На мой взгляд, скриншотные тесты обязательны для UIKit и для каких-то продуктовых сложных компонентов. Их фишка в том, что данные тесты могут использовать заготовленные для нас хелперы, data-testid атрибуты из интеграционных тестов.
Ниже пример скриншотного теста. Мы используем кастомный Matcher для того чтобы делать скриншот.
describe('feature/Сat', () => < it('должен рендерить в начальном состоянии', async () =>< mountComponent(< Component: AwesomeCat >); expect(await page.screenshot()).toMatchImageSnapshot(); >); >);
Финальные напутствия по части тестов для фронтенда
В заключение я собрал немного информации и видео по теме.
- Документация testing-library.
- Мои заметки в телеграме: RTL и Как пишем компонентные тесты.
- Доклад с конференции “Подлодка” про эффективное тестирование.
- Доклад про testing-library от моего бывшего коллеги Василия Кузенкова.
- Доклад с HolyJS про скриншотное тестирование.
Код проекта, на примере которого мы разбирались с тестами в это статье – я выложил на GitHub, заглядывайте.
Если вы на своём проекте не пишете юнит-тесты, то попробуйте написать. Я уверен, что у вас есть какой-то фрагмент кода, про который никто не знает, как он работает, либо постоянно там случаются какие-то баги. Я думаю, если вы напишете юнит-тесты, то ваша команда будет вам благодарна.
Если есть время, то можно попробовать написать интеграционный тест. По секрету скажу, что необязательно описывать пейдж-объекты, data-testid атрибуты и всё остальное; достаточно подключить testing-library и начать тестировать какие-то пользовательские сценарии на небольших компонентах. Если у вас есть в проекте UIKit, попробуйте скриншотные тесты.
Как писал Кент Си Доддс: «Пишите тесты, не слишком много, и больше интеграционных». Спасибо, что прочитали!
- javascript
- тестирование
- Блог компании Samokat.tech
- Тестирование IT-систем
- JavaScript
- Тестирование мобильных приложений
- TypeScript
Тестирование фронтенда
То, что видно на экране, – единственное, что имеет значение для конечных пользователей. Компании необходимо проверить, как выглядит и функционирует сайт, прежде чем он будет запущен в эксплуатацию. Чтобы обеспечить безупречный графический интерфейс пользователя (GUI), необходимо провести тестирование фронтенда (frontend testing).
В этой статье рассмотрено, что такое тестирование фронтенда, его виды, важность и чем оно отличается от тестирования бэкенда (backend testing).
Друзья, поддержите нас вступлением в наш телеграм канал QaRocks. Там много туториалов, задач по автоматизации и книг по QA.
Что такое тестирование фронтенда?
Фронтенд – это клиентская часть программы. Можно сказать, что она включает в себя все, что видит пользователь при взаимодействии с приложением. Любое веб-приложение имеет трехуровневую архитектуру: клиент, сервер и база данных. Презентационный слой включает в себя клиента. Тестировщики фронтенда тестируют этот слой. Они выполняют тестирование графического интерфейса и удобства использования сайта или приложения.
Например, нужно протестировать приложение для покупок. Проверки будут включать оценку соответствия внешнего вида сайта требованиям клиента и работы таких необходимых функций, как поиск и добавление товара в корзину. Тестирование фронтенда охватывает широкий спектр стратегий и техник, но прежде чем погрузиться в эту тему, следует рассмотреть, чем тестирование фронтенда отличается от тестирования бэкенда.
Чем отличаются фронт- и бэкенд тестирование?
Бэкенд приложения включает в себя серверный уровень и базу данных. Другими словами, в нем заложены функциональность и бизнес-логика, обеспечивающие работу приложения.
Тестирование бэкенда не затрагивает пользовательский интерфейс (UI) приложения. В основном оно проверяет, как в базе данных хранится информация, поступающая от клиента или сервера. Кроме того, тестировщики оценивают, как отправляются запросы к базе данных и какие ответы приходят на сервер.
Итак, в чем же основные различия двух видов тестирования?
Знания
В первую очередь тестировщики фронтенда работают с требованиями заказчика. Кроме того, им необходим опыт работы с фреймворками автоматизации для оптимизации процесса тестирования. Тестировщики бэкенда, в свою очередь, должны обладать знаниями о базах данных и SQL-запросах.
Что тестируется
Тестировщики фронтенда проверяют, соответствует ли графический интерфейс приложения требованиям. Кроме того, они должны оценить каждый элемент, например, кнопки, ярлыки, поля ввода и т.д.
Тестирование бэкенда включает в себя работу с базой данных, при этом графический интерфейс может и не использоваться. Тестировщики проверяют, нет ли потери данных или взаимной блокировки.
Инструменты
К числу важных инструментов для тестирования фронтенда относятся Grunt, Karma, Mocha и многие другие. Популярными инструментами для тестирования бэкенда являются TurboData, Data Generator и другие инструменты для тестирования API.
Зачем тестировать фронтенд?
Конечные пользователи не очень хорошо представляют себе, как работает бэкенд. Они сталкиваются только с теми проблемами, которые возникают в пользовательском интерфейсе. Чтобы быть успешным, приложение должно быть быстрым и работать без сбоев. Именно здесь на помощь приходит тестирование фронтенда, при котором оценивается работа программы на различных устройствах и браузерах.
W3C (The World Wide Web Consortium) ввел новые критерии, касающиеся дизайна, юзабилити и доступности веб-приложений. HTML-код должен соответствовать определенным стандартам, а сайт — быть доступен для всех, особенно для людей с ограниченными возможностями. Вот почему тестирование на доступность обязательно входит в список проверок фронтенда.
Кроме того, Интернет вещей (IoT) вывел разработку приложений на качественно новый уровень. С широким распространением взаимосвязанных приложений на смарт-часах, смартфонах и “умных” телевизорах тестирование фронтенда стало необходимым для проверки поведения программы в многоуровневой архитектуре.
Типы тестирования фронтенда
Разработчики отвечают за согласованную и стабильную работу продукта. Но они не могут добиться этого без тестировщиков. Тестирование фронтенда охватывает множество стратегий. Чтобы понять, что лучше всего подходит для вашего проекта, необходимо знать типы тестирования фронтенда.
Модульное тестирование (Unit Testing)
Каждый фрагмент кода должен функционировать независимо. Под модулем понимается наименьшая часть программного обеспечения, которую можно тестировать. Модульное тестирование – это самый низкий уровень тестирования. Здесь тестировщики проверяют отдельные компоненты программы или приложения. Данный тип тестирования позволяет убедиться в том, что отдельные части кода работают в соответствии с ожиданиями. Модульное тестирование включает в себя вычисления и проверку входных данных.
Приемочное тестирование
При приемочном тестировании тестировщики оценивают соответствие системы бизнес-требованиям. После этого они проверяют, насколько продукт готов к запуску. Например, если вы собираете дом из конструктора Lego, вы проверяете, идеально ли подогнана каждая деталь. Это относится к модульному тестированию. Следующий шаг – убедиться, что все инструкции из требований были выполнены. Приемочные тесты сканируют работающее приложение. Они проверяют правильность логики User flow, реакций приложения на ввод определенных данных и т.д.
Регрессионное тестирование
Всякий раз, когда в приложение вносится какое-либо изменение, существует вероятность того, что какая-то из ранее существовавших функций может выйти из строя. Вот тут-то и пригодится регрессионное тестирование. Визуальное регрессионное тестирование – это еще один вид регрессионного тестирования, связанный с пользовательским интерфейсом. Оно предполагает создание скриншотов пользовательского интерфейса и сравнение их с предыдущими изображениями. Визуальное регрессионное тестирование характерно только для фронтенда. Тестировщики используют специальные инструменты сравнения изображений для выявления различий между двумя снимками.
Тестирование доступности
Как было сказано выше, тестирование доступности призвано обеспечить возможность работать с приложением всем желающим, в том числе и людям с ограниченными возможностями. К ним относятся пользователи старшего возраста, а также люди с нарушениями слуха и зрения. Тестирование доступности в основном включает в себя проверку совместимости приложения с программами чтения с экрана (screen reader).
Тестирование производительности
Производительность сайта или приложения имеет первостепенное значение для пользователей и бизнеса. Тестирование производительности определяет стабильность, отзывчивость и скорость работы продукта. Кроме того, оно позволяет определить, как работает устройство в определенных условиях. Существует множество инструментов для тестирования производительности. Большинство из них работают по принципу “подключи и работай“. Однако некоторые инструменты позволяют кастомизировать ход выполнения тестов.
Сквозное тестирование (End-to-End Testing)
Сквозное тестирование – это валидация приложения полностью, от начала до конца, то есть оно гарантирует, что приложение будет вести себя в соответствии с ожиданиями на протяжении всего времени его работы. Оно также выявляет системные зависимости и обеспечивает целостность данных.
Интеграционное тестирование
Интеграционное тестирование объединяет различные блоки приложения и тестирует их как единое целое. Можно протестировать отдельные функциональности и убедиться, что они работают без дефектов, но это не гарантирует, что после интеграции не возникнут проблемы. Например, выпадающий список на сайте может перестать работать после того, как он будет интегрирован в панель навигации. В случае, если какая-то функциональность еще не готова или к ней нет доступа, используются тестовые заглушки и тестовые драйверы.
Тестирование кросс-браузерной совместимости
Тестирование кросс-браузерной совместимости является одним из важнейших типов тестирования фронтенда. Оно направлено на обеспечение одинаковых возможностей для пользователей различных браузеров, ОС и устройств. Иными словами, функции, доступные в одном браузере, должны быть доступны и в других.
Как создать тест-план?
План тестирования помогает определить потребности проекта и выстроить оптимальную стратегию процесса. Ниже рассмотрены основные нюансы, на которые стоит обратить внимание при создании тест-плана.
Подсчет бюджета
Необходимо заранее рассчитать, какие суммы будут затрачены на необходимые инструменты и работу сотрудников. Даже если бюджет ограничен, можно обеспечить высокое качество тестирования путем оптимизации расходов. Например, если приложению требуется кросс-браузерное тестирование, можно сэкономить на покупке различных устройств, браузеров или операционных систем, воспользовавшись облачной платформой.
Подбор инструментов
По мере того как происходит подсчет бюджета, необходимо составить список инструментов, которые будут использованы в ходе тестирования. При тестировании фронтенда возникает множество задач, и не всегда их можно решить с помощью одного инструмента. Заранее принятое решение о том, какие инструменты будут использованы, будут ли они платными или бесплатными, поможет ускорить процесс тестирования и точнее рассчитать затраты.
Определение временных рамок
Работа по Agile методологии подразумевает установку жестких временных рамок. При тестировании фронтенда необходимо охватить множество аспектов, в связи с чем нехватка времени может стать серьезным ограничением. Вот почему важно заранее определить, сколько времени понадобится для каждого этапа тестирования.
Оценка масштабности проекта
Существуют различные браузеры, ОС и устройства. Для обеспечения высокого уровня тестирования необходимо знать, чем пользуется целевая аудитория, чтобы верно определить масштабность проекта. Это поможет подобрать оптимальные инструменты тестирования и в конечном итоге сократить расходы и время на разработку.
Популярные инструменты для тестирования фронтенда
Различные инструменты тестирования выполняют различные функции. В связи с этим выделяют следующие типы инструментов:
Инструменты для запуска тестов (Test Launchers)
Запустить тест в Node.js или браузере можно с помощью средств запуска тестов. Они используют интерфейс командной строки (CLI) или пользовательский интерфейс (UI) с пользовательской конфигурацией. Инструменты для запуска тестов входят в состав различных фреймворков, например, Karma, Jest, Jasmine и др.
Assert функции
Инструкции assert — это булевы выражения, которые проверяют, является ли условие истинным. Они определяют, соответствуют ли результаты тестирования ожиданиям. Среди инструментов для работы с assert функциями можно назвать Chai, Jest, Jasmine и TestCafe.
Отслеживание выполнения тестов
Еще одной обязательной задачей тестировщика является проверка хода выполнения теста. Поэтому тестировщики используют инструменты отслеживания состояния запланированного тестирования с помощью встроенных отчетов. Данная функция доступна в таких фреймворках, как Mocha, Jest, TestCafe, Jasmine и Karma.
Моки (Mocks) и заглушки (Stubs)
В модульном тестировании, чтобы уловить побочные эффекты тестов, необходимо изолировать некоторые части тестируемого приложения. Для этого используют моки (имитаторы), заглушки и их комбинации. Подобные функции обеспечивают Sinon, Enzyme, Jest, Jasmine и др.
Сравнение скриншотов
Сравнение скриншотов применяется для проверки правильности реализации изменений. Для этого используются инструменты Jest и Ava. Для автоматизированного сравнения снимков существует такая многофункциональная платформа, как Testim.
Покрытие кода
Каждый тест охватывает определенную часть кода. Покрытие кода – это показатель того, какой объем кода покрывают тесты. Parasoft Jtest и Cobertura – инструменты, позволяющие вычислить данный показатель.
Контроллеры браузера
Для функциональных тестов имитация действий пользователя возможна с помощью контроллеров браузера. Для этого тестировщики используют комбинацию различных инструментов. К ним относятся TestCafe, Nightwatch и Puppeteer.
Визуальная регрессия
Инструменты визуальной регрессии позволяют сравнить сайт с его предыдущей версией. К ним относятся Applitools, Percy и WebdriverCSS.
Проблемы тестирования фронтенда и способы их решения
Любая работа, будь то разработка или тестирование, сопряжена с определенными трудностями. Тестирование фронтенда не является исключением. Ниже рассмотрены некоторые общие проблемы и способы их решения.
Команда впервые использует автоматизацию
Тестирование фронтенда, особенно кросс-браузерное и регрессионное, требует автоматизации. Если команда использует автоматизацию впервые, она может столкнуться с трудностями при настройке и написании многократно используемых тест-кейсов. Перевод большого количества тест-кейсов из ручного тестирования в автоматизированное может занять много времени. В худшем случае команда не успеет завершить тестирование.
Чтобы избежать этого, следует начать подготовку еще до начала проекта и обучить команду тем фреймворкам автоматизации, которые будут использованы на проекте. Для оптимизации времязатрат тестировщики могут подготовить тест-кейсы до того, как команда разработчиков завершит свою работу.
Имитация “реального мира”
Еще более серьезной проблемой является то, что тестировщики не всегда могут угадать, как поведет себя реальный пользователь при работе с приложением. Трудно предсказать сценарии, отражающие то, как пользователь будет взаимодействовать с той или иной страницей или функцией.
Тестирование удобства использования позволяет оценить, как реальный пользователь будет использовать приложение. Можно попросить коллег или других членов команды воспользоваться приложением в качестве тест-группы. Это поможет убедиться, что не осталось сценариев, которые пропустила команда тестировщиков при написании тест-кейсов. Также стоит уделить внимание тестированию производительности. Оно покажет, как ведет себя приложение при одновременной работе с ним большого числа пользователей. Тестирование производительности позволит убедиться в том, что приложение готово к внезапному наплыву пользователей или большому количеству интернет-трафика.
Agile и регрессия
Agile предоставляет заказчику большую свободу действий. После завершения каждого спринта клиент может проверить приложение и попросить внести какие-либо изменения. Он может попросить вашу команду добавить определенную функцию или убрать ту, которая ему не нравится. При частых изменениях в приложении возрастает риск возникновения регрессионных ошибок.
Важно проводить регрессионное тестирование каждый раз после того, как разработчик выполнит запрос на изменение. Можно использовать инструменты визуальной регрессии и/или сравнения скриншотов. Эти инструменты позволят команде тестирования легко найти любые регрессионные ошибки, что значительно снизит вероятность возникновения значимых дефектов при развертывании приложения на сервере.
Фронтенд или бэкенд?
После обнаружения и заведения бага тестировщик должен назначить дефект на определенного разработчика для исправления. Например, было обнаружено, что одна ячейка в таблице базы данных отображает неверные данные. Сотрудник, отвечающий за фронтенд, может решить, что это проблема на стороне бэкенда, то есть код не может обеспечить получение корректных данных из базы. В свою очередь, разработчик бэкенда может сказать, что с его стороны код корректен, а баг кроется в коде фронтенда.
Чтобы предотвратить подобные проблемы, необходимо поощрять команду следовать лучшим практикам. Пусть они думают и работают как Agile-команда. В Agile тестировщики должны быть осведомлены о некоторых особенностях разработки, равно как бэкенд и фронтенд разработчики должны иметь представление о работе друг друга. Если тестировщик по ошибке назначает дефект не тому человеку, то этот человек должен разобраться в ошибке, найти ее первопричину и попросить соответствующего специалиста исправить ее.
Заключение
Даже самые незначительные ошибки могут повлиять на репутацию компании. Если пользователи заметят небольшую задержку или лаг на сайте или в приложении, они не задумываясь перейдут к конкурентам, предлагающим более качественный продукт. Несомненно, это делает тестирование фронтенда еще более важным.
Похожие записи:
- Тестирование API
- Что такое спайк-тестирование
- Исследовательское тестирование и его преимущества
- Тестирование доступности веб-приложений
Тестирование фронтенда: большой гайд
Что такое фронтенд? Это «лицо» системы, графический интерфейс пользователя (GUI) веб-сайта или веб-приложения — посредством которого пользователь взаимодействует с веб-сайтом или веб-приложением.
Иными словами, фронтенд является как бы публичной частью или, если понимать буквально, «передним концом» ИТ-продукта, то есть той частью цифрового продукта, которая всегда «лицом к пользователю».
Таким образом, тестирование фронтенда (frontend testing) — это проверка юзабельности и функциональности интерфейса сайта/приложения.
Проще говоря, проверка вида и срабатывания меню, форм, кнопок и других элементов, с которыми работает пользователь/клиент.
Почему это необходимость
При разработке сайта (приложения) его создатели хотят убедиться, что все работает корректно и «не будет сюрпризов на проде». И что клиент (пользователь) останется доволен продуктом.
Этого достигают, концентрируя усилия на легкости и простоте использования, идут по тому же пути что и пользователь «идет по приложению», то есть оптимизируют навигацию, пытаются её упростить и приспособить под нужды предполагаемого «среднего пользователя».
Есть в качественном тестировании фронтенда и интерес разработчиков. Кроме надежного деплоя на проде (то есть приложение должно гарантированно работать), нужно обеспечить стабильную работоспособность приложения/сайта на протяжении всего пути пользователя (таким образом обеспечив покрытие бОльшей части значимых дефектов, то есть гарантировать работоспособность «в целом»).
То есть нужно обеспечить работоспособность в пограничных случаях (edge cases), уязвимых и/или критически важных частях приложения/сайта. Обращая особое внимание (ведь речь идет о фронтенде с его особенностями) на «бесконечные загрузки» или «состояния ошибки», из которых пользователь не знает как выйти.
В зависимости от ситуации и от имеющихся требований, фронтенд могут тестировать вручную, или автоматизировать все или большую часть тестовых операций. Суть тестирования фронтенда — проверка клиентской («браузерной») части приложения (сайта) — и эти операции успешно автоматизируются.
Пример. На любом сайте есть кнопки отправки пользовательских данных из формы (или выбора варианта, etc), и нужно верифицировать выполнение этих действий; а также всегда есть гиперссылки, нажатие на любую из них также проверяется; нажатие кнопки влечет за собой какие-то действия на сервере — обработку данных; тестирование обработки на сервере уже является частью тестирования бекенда.
Далее, качественно выполненное тестирование фронтенда обеспечивает надежную защиту от избыточного регресса, то есть излишнего количества операций при регрессионном тестировании. То есть, когда добавлен код сложной необходимой функции, но в части фронтенда не протестирован, и в результате возникли проблемы с другими частями сайта/приложения.
Случается также, что разработчики забывают о проблемном коде, написанном давно, и кое-как протестированном, и добавляют новый код к существующему, что приводит к проблемам на фронтенде. Так бывает в проектах с большим количеством команд и разработчиков. Современные приложения сложные, состоят из огромного количества компонентов, никому не получится реализовать большой проект в одиночку или небольшой командой; и никто не в состоянии знать всё о каждой части кодовой базы. Поэтому тестирование фронтенда — это также проверка, что одни части кода не конфликтуют с другими, и что функциональность на фронтенде в идеальном состоянии.
Таким образом, формируется уверенность в качестве кода — исключаются ситуации, когда пользователи не смогут воспользоваться приложением, или раздраженно удалят его после нескольких минут использования.
QA-департамент вовремя найдет ошибку на фронтенде, или это сделает бета-пользователь (скажем на альфа- и бета-стадиях), ошибка будет вовремя исправлена, и до конечного пользователя она не дойдет. Далее этот дефект будет покрыт повторным тест-сьютом, чтобы 100%-но гарантировать, что он потом не проявится снова.
Итак, чем лучше, качественнее проведено фронтенд-тестирование, чем легче и быстрее будет проходить развертывание (деплой). Руководство департаментов и стейкхолдеры, несомненно, будут удовлетворены, зная что разработка идёт в стабильном и прогнозируемом темпе. Это позволит им надежнее планировать операции департамента.
Ещё одна веская причина качественно проводить фронтенд-тестирование: такие тесты выступают в качестве live-документации. Написание тестов требует понимания логики проекта и правильного описания, что этот конкретный тест делает, и с чем связан тестируемый компонент приложения.
Тесты фронтенда часто включают моки к API компонентов, которые могут выступать в качестве гайдлайнов для разработчиков в будущем. В отличие от написанной вручную документации в виде README-файлов, такие гайдлайны «живут внутри проекта». Если тест просрочился/устарел, он возможно приведет к ошибкам, которые заставят обновить документацию, что часто забывают делать, если она пишется вручную.
Составление тест-сьютов на фронтенде (грамотное группирование тест-кейсов фронтенда) может, и должно, улучшать читабельность и устранять дублирование кода.
Когда разработчику понадобится протестировать небольшую часть кода, и окажется что нужно подтягивать еще излишние моки/компоненты, это заставит рефакторить такой код.
«Чистый код» лучше тестируется, а хороший тестабельный код — как правило «чистый». Лучше для фронтенд-разработчиков, и в конечном счете для конечных пользователей.
Что тестируют на фронтенде
Оптимизируют производительность и улучшают user experience.
До наступления эпохи web 2.0 веб-приложения были статическими. То есть обработка данных производилась на стороне сервера (бекенде), и оптимизация производительности тоже на бекенде. Теперь веб-приложения динамические, значит нужно оптимизировать код фронтенда.
Фокус на таких вещах:
- Кроссбраузерная и кроссплатформенная функциональность — проверка функциональности и респонсивности приложения на разных платформах, девайсах и браузерах.
- Доступность — проверка доступности приложения любому пользователю, включая не только людей с визуальными или слуховыми проблемами, но и например для водителей во время движения.
- Сквозное тестирование — для проверки пользовательских путей по приложению end-to-end, воспроизводя действия реальных пользователей.
- Тестирование отображения рисунков и фото — стандартный сайт или веб-приложение сейчас подгружает множество изображений, в том числе в высоком разрешении; баннеры, логотипы, фотографии. Изображения значительно, в десятки и сотни раз, увеличивают размер среднего приложения (сайта). И, разумеется, для проверки корректности их отображения желательно проводить соответствующее тестирование, с последующей оптимизацией изображений, и при необходимости коррекцией релевантного кода сайта/приложения.
- Тестирование CSS — каскадные таблицы стилей, если прописаны некорректно, ухудшают user experience, поэтому бывает нужна проверка синтаксиса и отображения CSS. Синтаксис это ответственность разработчиков, а отображение CSS — тестировщиков; обычно отображение тестируется как часть регресса после изменений.
Челенджи
Определить самые важные части фронтенда
Тестирование фронтенда затрагивает множество компонентов UI в самых разных сочетаниях в зависимости от проекта: форматирование, видимый текст, графика и CSS. Также функциональные аспекты: кнопки, формы, ссылки.
Проверяют респонсивность, как быстро загружаются компоненты UI и как реагируют; комфортно ли время отклика на действия пользователя, как быстро обрабатываются пользовательские функции, возвращают ли желаемый результат.
Имея перед собой такой объём компонентов, нужно приоритизировать эти проверки. В стандартном виде: присвоить приоритеты группам компонентов, начиная с основного текста, скорости загрузки страниц, видимости и доступности критически важных элементов, которые должны быть на своих местах и работать безукоризненно. Далее убедиться, что другие важные функциональные элементы видимы и отвечают на все действия и запросы. Завершаем проверкой форматирования и качества графики.
Эмуляция реального окружения
Заметную сложность составляет эмуляция работы приложения в реальном окружении. Тестовое окружение всегда контролируется, в отличие от реального, поэтому тяжело составить правильное впечатление, как будет себя вести приложение/сайт вне контролируемого окружения.
Хотя и сложно воссоздать реальное окружение для всех возможных пользователей, вполне реально подготовить тесты, достаточно близко воспроизводящие условия, в которых работает большинство пользователей. Особенно что касается производительности: можно эмулировать нагрузку большого количества одновременных пользователей и запросов. Также можно лимитировать доступную память, мощность процессора, или скорость подключения к сети.
Инструменты
Об инструментах будет подробно в конце, сейчас же кратко обозначим важный момент: при выборе инструментов следует сосредоточиться на правильном наборе функций. Типичный инструмент тестирования фронтенда включает:
- Простое создание и обслуживание тестовых сценариев
- Инсайты по метриках производительности
- Хорошие возможности по репортам
- Бесшовная интеграция с другими инструментами
- И конечно автоматизация
Автоматизация подразумевается по умолчанию, вручную выполнять все тесты с каждым апдейтом нереально.
Человеческий фактор
Существует «софт»-фактор, влияющий на процесс, а именно люди, исполнители. Здесь не имеется в виду, что тестировщики обязательно будут ошибаться в процессе; скорее, что существует вероятность того что разработчики будут склонны пропускать какие-то тесты или этапы цикла по своим причинам. Или же, клиенты могут влиять на процесс, заставляя ускорять релиз. Или руководство проекта, видя недостаточность ресурсов или исполнителей, примет решение изменить процесс.
По некоторым причинам в компании могут существенно уменьшить выделенные ресурсы на фронтенд-тестирование; здесь может быть уместной метафора «разбитого окна»: утверждается, что если проблема видима, но не устраняется, она становится привычной, хоть и не раздражающей, пока ситуация не придет к тому, что проблема становится всеобъемлющей. Применяя метафору к тестированию фронтенда, можно предполагать, что если такое тестирование проведено небрежно, и код написан некорректный, или его состояние неизвестно, эта плохая практика будет воспроизводиться в будущем, а проблемы в продукте будут накапливаться.
Лучшие подходы
Желательно соблюдать проверенные практики при написании тестов.
F.I.R.S.T-принципы
Хороший тест — это:
- Быстрый
- Изолированный (то есть независимый от других)
- Воспроизводимый
- Само-валидируйщийся
- Полный (основательный)
Большинство этих понятий самоочевидны, но уточним: тесты должны быть быстрыми (на любом этапе цикла), изолированными (в том числе от не протестированных компонентов), легко воспроизводимыми в будущем, способными валидировать себя (независимо от того прошел тест или нет), и полными в том смысле, что покрывают все нужные переменные.
Тестовая пирамида должна быть пирамидой
А не чем-то другим (о чем-то другом у нас есть соответствующий материал). Принцип должен соблюдаться, и так процесс будет наиболее правильным. У разработчиков должно хватать времени и желания на качественное выполнение своей части работы — чтобы руководству не пришлось потом во всём полагаться на тестировщиков, заставляя их делать например сложные сквозные в большом количестве, или вообще нагружая несвойственной им работой. Количество юнит-тестов должно быть самым большим; интеграционных уже намного меньше; а сквозных вообще мало, в идеале только критические пользовательские пути.
Приоритизация элементов фронтенда
Фронтенд — это могут быть даже сотни тысяч отдельных элементов UI (функций). Элементы это CSS, текст, графика, формы, линки, кнопки. Разумеется, все это проверить невозможно, без приоритизации не обойтись. В приоритете должны быть: скорость загрузки страницы, базовый (видимый пользователю) текст, изображения (иконки и логотипы), далее главные функции (например в случае магазина — добавление в корзину и оплата), далее графика по ситуации и всплывающие окна, если таковые имеются. Приоритетные элементы должны быть в любом случае видимые и респонсивные. И завершаем остальной графикой и форматированием.
Реальные браузеры и девайсы
Очень желательно, если стоит задача обеспечить надежность, использовать реальные устройства и браузеры — тогда тестовое окружение (почти полностью) соответствует реальному. Эмуляторы и симуляторы, безусловно, дают временную экономию времени и средств — но потом возможно придется переписывать код и еще раз тестировать.
Разновидности
Юнит-тестирование
Фундаментальный уровень. Анализ компонентов и функций и проверка их поведения. Как упоминалось выше, в разделе лучших практик, этому уровню необходимо уделять внимание, это укрепляет стабильность кодовой базы и надежность приложения после релиза.
Приемочное
Подтверждение пользовательских вводов, пользовательских путей (то есть user flow), и присвоенных им действий. Выполняется приёмка продукта, чтобы убедиться что финальная версия работает так, как ожидалось при проектировании, как ожидают конечные пользователи.
Визуальное регрессионное
Особый подвид фронтенд-тестов. Сравнивается имеющийся интерфейс с ожидаемым (спроектированным), определяя недостатки/проблемы. В общем случае сопоставляют скриншоты из headless-браузера, запущенного на сервере, и на условно пользовательской машине, сравнивая изображения и затем вносят корректировки.
Доступности
Об этом уже было выше; разновидность юзабилити-тестирования, когда ищут проблемы, мешающие каким-то категориям людей пользоваться продуктом, его отдельными функциями, особенно недостатки в навигации.
Производительности
Анализ производительности сайта/приложения, изучение скорости загрузки, стабильности, масштабируемости, совместимости и респонсивности («отзывчивости» на действия пользователя, плавности интерфейса в сочетании с комфортной его скоростью). Приложение (или сайт) с плохой производительностью, неспособное к расширению пользовательской базы, и грубо говоря тормозящее — не оправдает надежд не только пользователей, но и руководства проекта.
Сквозное
От начала до конца по пути пользователя, имитируя его действия, нащупывая подводные камни, особенно в интерфейсе и API. При этом получают инсайт о всей системе в целом.
Интеграционное
Все современные приложения состоят из множества связанных модулей, и качество связей проверяет интеграционное фронтенд-тестирование.
Кроссбраузерное
Собственно, уже сказано выше; один и тот же набор тест-кейсов выполняется в разных браузерах. Успешно автоматизируется.
Инструменты
Перейдем к инструментам. Возможных инструментов, применимых на фронтенде, существуют сотни. Попытаемся обозначить самые распространенные.
Jest
Вероятно, самый популярный JS-фреймворк (после Selenium) с упором на скорость и простоту применения. Параллельное выполнение тестов. Умеет сначала запускать падавшие ранее тесты, и реорганизовать запуски смотря по времени выполнения. Обеспечивает большое тестовое покрытие и удобную работу с моками.
Selenium WebDriver
Кроссбраузерные тесты. Selenium — все еще инструмент веб-тестировщика №1. Поддерживает все языки.
Cypress
Фреймворк автоматизации веб-тестов на JS. Любят и разработчики, и тестировщики (пруф), видимо из-за простого синтаксиса.
WebDriverIO
Автоматизация «веба и мобайла». Простые операции с тестируемым приложением, набор плагинов.
WebDriverJS
Официальная JS-имплементация Selenium, использующая JSON-wire-протокол для работы с браузерами. Почти то же, что Selenium WebDriver.
TestCafe
Node.js-инструмент для автоматизации, открытый, бесплатный, есть end-to-end-возможности.
План при тестировании фронтенда
Бюджет
Перед тем как выбрать инструменты и назначить команду, проджект-менеджер уточняет бюджет проекта.
Инструменты
Зависит от особенностей фронтенда в проекте, и от требований.
Таймлайн
Здесь нужна гибкость, ведь может получиться, что приложение выпущено в срок, а по факту некачественное.
Область тестирования
Должен ли проект быть выпущен в состоянии близком к идеальному, или, возможно, будет достаточно минимально рабочей версии; будет ли учитываться фидбек пользователей, планируется ли быстрое расширение.