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

Testing Library Dom Тестирование фронтенда


theme: gaia class:

  • lead
  • invert paginate: true ---

Dom Testing Library

Hexlet


Экосистема Testing Library

  • dom testing library — основная библиотека
  • user-event — имитация браузерных событий
  • jest-dom — кастомные матчеры Jest
  • eslint-plugin-testing-library — плагин ESLint для Testing Library
  • eslint-plugin-jest-dom — плагин ESLint для Jest DOM
  • Версии для фреймворков
    • react testing library
    • angular testing library
    • svelte testing library

Ключевой руководящий принцип

Чем лучше ваши тесты имитируют реальное использование вашего приложения, тем больше уверенности они могут вам дать


  • тестируйте UI компоненты с точки зрения пользователя
  • избегайте проверки деталей реализации
  • не полагайтесь на конкретный фреймворк для тестирования
  • не используйте test runner
  • DOM Testing Library работает с любым окружением, где есть DOM API
    • Jest, Mocha
    • JSDOM
    • реальный браузер


test("greeting", () => {
  const h1 = queryByText(/Hello/i);
  expect(h1).toHaveTextContent("Hello username!");
  expect(queryByText("Goodbye!")).not.toBeInTheDocument();
});


Запросы

  • get, getAll
  • query, queryAll
  • find, findAll

No Match1 Match1+ MatchAwait?
getBythrowreturnthrowNo
findBythrowreturnthrowYes
queryBynullreturnthrowNo
getAllBythrowarrayarrayNo
findAllBythrowarrayarrayYes
queryAllBy[]arrayarrayNo

ByRole

  • Селектор элементов по роли
  • getByRole, getAllByRole
  • queryByRole queryAllByRole
  • findByRole, findAllByRole
  • Доступные роли: link, button, form, heading, document, img, checkbox, radio, listitem, main, navigation, table, textbox
  • getByRole(expectedRole, { name: /submit/i })
  • getByRole('checkbox', { checked: true })
  • getAllByRole('button', { hidden: true })


test('show a required field warning', async () => {
  const loginButton = getByRole(container, 'button', {name: 'Login'});
  loginButton.click();

  expect(await findByText(container, 'User Name Required')).toBeVisible();
  expect(await findByText(container, 'Password Required')).toBeVisible();
});


ByText


<a href="/about">About ℹ️</a>

import { getByText } from '@testing-library/dom';
const aboutLinkNode = getByText(document.body, /about/i);

// ИЛИ
import { screen } from '@testing-library/dom';
const aboutLinkNode = screen.getByText(/about/i);

TextMatch

  • как строка
    • screen.getByText("Hello World") - поиск по полной строке
    • screen.getByText('llo worl', { exact: false }) — поиск по подстроке, игнорируется регистр
  • как регулярное выражение
    • screen.getByText(/world/i) — поиск по подстроке, игнорируется регистр
    • screen.getByText(/^hello world$/i) — поиск по полной строке, игнорируется регистр
  • как функция
    • screen.getByText((content, element) => content.startsWith("Hello"))

  • screen.getByLabelText('Username') ищет элемент с соответствющим label
  • getByLabelText хорошо подходит для полей формы
  • getByPlaceholderText ищет по атрибуту placeholder
  • getByTitle ищет по атрибуту title

<div id="app">
  <label for="username-input">Username</label>
  <input id="username-input" />
</div>
import { getByLabelText } from "@testing-library/dom";

const container = document.querySelector("#app");
const inputNode = getByLabelText(container, "Username");

// ИЛИ
import { screen } from "@testing-library/dom";

const inputNode = screen.getByLabelText("Username");

ByTestId

<div data-testid="mailbox" />
const element = document.body.querySelector(`[data-testid="mailbox"]`)

// OR
import { screen } from '@testing-library/dom';
const element = screen.getByTestId('mailbox');
  • configure({testIdAttribute: 'data-my-test-attribute'})

Расширение для браузеров Chrome и Firefox Testing Playground


Вызов событий


fireEvent(node: HTMLElement, event: Event)


<button>Submit</button>
fireEvent(
  screen.getByText("Submit"),
  new MouseEvent("click", {
    bubbles: true,
    cancelable: true,
  })
);

fireEvent[eventName](node: HTMLElement, eventProperties: Object)



fireEvent.change(getByPlaceholderText(/username/i), { target: { value: 'ruslan' } });

fireEvent.keyDown(getByRole('button'), { key: 'Enter', code: 'Enter' });


Асинхронность


const button = screen.getByRole('button', { name: 'Login' });
fireEvent.click(button);
await screen.findByText('Hello');


findBy = getBy + waitFor


waitFor

function waitFor<T>(
  callback: () => T | Promise<T>,
  options?: {
    container?: HTMLElement // document по умолчанию
    timeout?: number // 1000мс по умолчанию
    interval?: number // 50мс по умолчанию
    onTimeout?: (error: Error) => Error // ошибка и состояние контейнера по умолчанию
    mutationObserverOptions?: MutationObserverInit // вызов колбека при изменениях
  }
): Promise<T>

  • await waitFor(() => screen.getByRole('alert'))

  • await waitFor(() => expect(mockAPI).toHaveBeenCalledTimes(1))

  • WaitForElementToBeRemoved

  • wrapper on waitFor


const button = document.querySelector('button');

waitForElementToBeRemoved(document.querySelector('button')).then(() =>
  console.log('Element button no longer in DOM');
);

// изменения игнорируются
button.setAttribute('data-is-submit', true);

// логирует строку 'Element button no longer in DOM'
button.parentElement.removeChild(button);

  • fireEvent подходит для большинства сценариев, НО
  • fireEvent.click не порождает другие события:
    • fireEvent.mouseOver(element)
    • fireEvent.mouseMove(element)
    • fireEvent.mouseDown(element)
    • element.focus() (если элемент допускает это)
    • fireEvent.mouseUp(element)
    • fireEvent.click(element)

Рефакторинг

// Было
fireEvent.focus(getByText('focus me'));
// Стало
getByText('focus me').focus();

user-event

Это вспомогательная библиотека для Testing Library, которая обеспечивает более совершенное моделирование взаимодействия с браузером, чем встроенный метод fireEvent


test("types inside textarea", () => {
  document.body.innerHTML = `<textarea />`;

  userEvent.type(screen.getByRole("textbox"), "Hello, World!");
  expect(screen.getByRole("textbox")).toHaveValue("Hello, World!");
});

test("click", () => {
  render(`
    <div>
      <label for="checkbox">Check</label>
      <input id="checkbox" type="checkbox" />
    </div>
  `);

  userEvent.click(screen.getByText("Check"));
  expect(screen.getByLabelText("Check")).toBeChecked();
});

user-event API

  • click / dbclick
  • hover / unhover
  • type
  • upload
  • selectOptions / deselectOptions
  • tab
  • paste

Click

click(element, eventInit, options)


Клики по элементу, могут иметь различные побочные эффекты в зависимости от элемента

<div>
  <label htmlFor="checkbox">Check</label>
  <input id="checkbox" type="checkbox" />
</div>
  userEvent.click(screen.getByText('Check'));
  expect(screen.getByLabelText('Check')).toBeChecked();

  • Клик вызовет сначала событие hover Его можно отключить установивtrue для параметра skipHover
  • Можно также вызывать клик с зажатыми клавишаи, ctrlClick / shiftClick / пр.
    • userEvent.click(elem, { ctrlKey: true, shiftKey: true })

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

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

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

Для полного доступа к курсу нужен базовый план

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

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

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

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

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

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

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

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

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»