Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Тесты JS: DOM API

Тесты, с которыми мы имели дело до этого курса, сильно отличаются от тех тестов, которые используются при проверке фронтенда.

Типичный тест выглядел так: импортируется необходимая функция, а затем вызывается с разными аргументами.

import sum from '../sum.js';

test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3);
});

Все просто и понятно. Конечно, если функция не чистая, то тест будет сложнее. Но в любом случае с этим можно разобраться.

Такие тесты называются юнит и интеграционными, в зависимости от того, сколько подсистем вовлечено в проверку. Причём невозможно провести чёткую границу между этими видами тестирования. Лучше воспринимать эту классификацию как шкалу, где слева — юниты, справа — интеграционные, а ваши тесты где-то между ними.

А как тестировать поведение кода в браузере? Ведь по сути основные действия, выполняемые таким кодом, это манипуляция с DOM.

Системные тесты

Тестировать такой код действительно можно. Для этого используется специальный софт, webdriver, который посылает команды из теста в браузер и возвращает результат обратно. То есть код теста имитирует настоящие действия пользователей и смотрит на то, как изменился DOM. Такой вид тестов называется системным.

import Nightmare from 'nightmare';

const nightmare = new Nightmare({ show: true });

test('duckduckgo', async () => {
  const link = await nightmare
    .goto('https://duckduckgo.com')
    .type('#search_form_input_homepage', 'github nightmare')
    .click('#search_button_homepage')
    .wait('#zero_click_wrapper .c-info__title a')
    .evaluate(() => document.querySelector('.c-info__title a').href);
  expect(link).toBe('www.nightmarejs.org');
});

В примере выше много тонких моментов, которые стоит разобрать.

Nightmare — это библиотека для системного тестирования. Внутри себя она использует конкретный драйвер, но для нас сейчас это не принципиально. Таких библиотек достаточно много, особенно в мире js. Конкретно nightmare, с одной стороны, популярная библиотека, с другой — позволяет запускаться в среде без графической оболочки, что важно при разработке.

В тесте первым делом мы указываем адрес страницы, которую необходимо открыть. Затем начинаем манипулировать этой страницей. Функция type() вводит текст по указанному селектору, a click(), очевидно, выполняет клик по элементу.

А вот что такое wait()? Как вы знаете, браузер работает асинхронно. После клика на элемент не происходит блокирования, мы можем продолжать работать дальше. Как только код выполнится, то, возможно, произойдут изменения на странице. А вот когда они произойдут, никто сказать не сможет. Всё, что мы можем делать, это постоянно опрашивать DOM на наличие требуемого изменения. wait() упрощает эту задачу. Эта функция принимает на вход селектор, за которым надо следить, и ждёт его появления. Если он появился, то управление передаётся дальше, если нет, то происходит ошибка. Время ожидания может настраиваться.

Дальше мы видим функцию evaluate(). Эта функция позволяет выполнить произвольный код внутри браузера. Подчеркну, тот код, который будет описан внутри функции, передающейся в evaluate(), выполняется не в том месте, где запустились тесты, а внутри браузера, с которым работает драйвер. Как правило, в этом месте извлекают данные, которые нужно проверить в тесте. evaluate() возвращает промис, из которого можно извлечь результат, используя механизм async/await.

Плюсы и минусы

Системные тесты, как правило, дают гораздо большие гарантии того, что ваша система работает. Поэтому их писать полезно на особо критические участки. Но покрывать ими всё "от и до" — занятие не для слабонервных. Минусов у этого вида тестирования вагон и маленькая тележка:

Тесты очень хрупкие

Это значит, что незначительные изменения в вёрстке приводят к тому, что они ломаются. Так как они завязаны на селекторы. Эту проблему частично можно нивелировать, используя так называемый паттерн Page Object. Его идея крайне проста. Давайте сделаем абстракцию над каждой страницей и будем работать через неё:

// LoginPage - абстракция над страницей входа
test('LoginPage', () => {
  // открываем страницу
  LoginPage.open();
  // устанавливаем имя пользователя
  LoginPage.username.setValue('foo');
  // устанавливаем пароль
  LoginPage.password.setValue('bar');
  // отправляем форму
  LoginPage.submit();
  // получаем результат отправки формы
  // и сравниваем его с ожидаемым ответом
  expect(LoginPage.flash.getText()).toBe('Your username is invalid!');
});

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

Сложно писать

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

Так же сложности добавляет невозможность легко писать в стиле TDD. По сути, тесты всегда пишутся после отладки кода через браузер.

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

Сложно отлаживать.

Типичный вывод провалившегося теста выглядит так:

Селектор ".title" на странице не найден

За этим выражением может скрываться всё что угодно: начиная от ошибки 500 и заканчивая тем, что у элемента стоит стиль display: none.

Долгое время выполнения

Просто как факт. Эти тесты выполняются крайне долго и большой тестовый набор может лопатить сайт часами и даже днями.

Разновидности системных тестов

Существуют и альтернативные подходы к системному тестированию. В основе тот же DOM, но проверки строятся по-другому.

Screenshot Testing

Screenshot Testing

Такой тест при первом запуске создаёт скриншот страницы, а при последующих сравнивает результаты новых запусков с исходным скриншотом. На рисунке выше слева это исходный скриншот (получаемый автоматически), посередине — результат очередного прогона, а справа — дифф.

Snapshot Testing

Еще один способ тестирования, популяризированный компанией facebook в их фреймворке jest. Он похож на предыдущий способ, с той разницей, что сравниваются не скриншоты, а результирующий dom.

test('application', () => {
  const element = document.querySelector('a[href="#profile"]');
  element.click();
  expect(getTree()).toMatchSnapshot();

  const element2 = document.querySelector('a[href="#settings"]');
  element2.click();
  expect(getTree()).toMatchSnapshot();

  const element3 = document.querySelector('a[href="#profile"]');
  element3.click();
  expect(getTree()).toMatchSnapshot();
});

Как видите, в тесте выше, нет точечных проверок. Есть только функция toMatchSnapshot. Вызов этой функции в первый раз (когда снепшота нет) создаёт эталонный снепшот в директории **tests/snapshots**, а повторные вызовы используют его для сравнения с текущим результатом. Из этого следует, что первый запуск снепшот тестов можно выполнять только тогда, когда мы уверены в работоспособности нашего кода.

Самое прекрасное в этой технике то, что она сильно упрощает анализ результата проверки. Ниже реальный пример из заданий на Хекслете:

Received value does not match stored snapshot 3.

    - Snapshot
    + Received

    @@ -18,11 +18,11 @@
         <!-- Tab panes -->
         <div class="tab-content m-3">
             <div class="tab-pane" id="home" role="tabpanel">
                 Home
             </div>
    -        <div class="tab-pane active" id="profile" role="tabpanel">
    +        <div class="tab-pane" id="profile" role="tabpanel">
                 Profile
             </div>

      at Object.<anonymous> (__tests__/application.test.js:43:21)

Вывод почти такой же, как и у git diff. Видно, что было не так и как должно было быть. Такой подход практически идеален для задач на Хекслете, так как сильно облегчает анализ для неподготовленных людей. Но за него приходится платить определённую цену. Так как снепшот содержит в себе всю страницу, то любое малейшее изменение dom приводит к тому, что всё ломается. По этой причине крайне важно, чтобы вёрстка была практически неизменяема. Банально, классы, добавленные в элемент не в той последовательности, приведут к тому, что тесты упадут.

Другими словами, snapshot testing — не панацея. Он отлично подходит для ситуаций, где DOM меняется редко и предсказуемо. Например, при разработке виджетов или в обучающих курсах.


Дополнительные материалы

  1. Начинаем писать тесты правильно (видео)
  2. Testing Library
  3. Как отлаживать упражнения на Хекслете

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты.

Ошибки, сложный материал, вопросы >
Нашли опечатку или неточность?

Выделите текст, нажмите ctrl + enter и отправьте его нам. В течение нескольких дней мы исправим ошибку или улучшим формулировку.

Что-то не получается или материал кажется сложным?

Загляните в раздел «Обсуждение»:

  • задайте вопрос. Вы быстрее справитесь с трудностями и прокачаете навык постановки правильных вопросов, что пригодится и в учёбе, и в работе программистом;
  • расскажите о своих впечатлениях. Если курс слишком сложный, подробный отзыв поможет нам сделать его лучше;
  • изучите вопросы других учеников и ответы на них. Это база знаний, которой можно и нужно пользоваться.
Об обучении на Хекслете

Для полного доступа к курсу нужна профессиональная подписка

Профессиональная подписка откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
900
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно.

  • 120 курсов, 2000+ часов теории
  • 900 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы соглашаетесь c «Политикой конфиденциальности» и «Условиями оказания услуг»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы

С нуля до разработчика. Возвращаем деньги, если не удалось найти работу.

Иконка программы Фронтенд-разработчик
Профессия
Разработка фронтенд-компонентов веб-приложений
29 сентября 8 месяцев

Есть вопрос или хотите участвовать в обсуждении?

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы соглашаетесь c «Политикой конфиденциальности» и «Условиями оказания услуг»