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

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

Testing Library Best Practice

Hexlet


Ограничения


import { render, screen, fireEvent } from '@testing-library/react';

test('clicks on disabled button', () => {
  const handleClick = jest.fn();

  render(
    <button style={{ pointerEvents: "none"}} onClick={handleClick}>
      Save
    </button>
  );

  fireEvent.click(screen.getByRole('button'));
  expect(handleClick).not.toBeCalled();
});

width:1100px


test('styles', () => {
  const handleClick = jest.fn();

  render(
    <div style={{position: 'absolute', left: '-300%'}}>
      <button onClick={handleClick}>
        Save
      </button>
    </div>
  );

  fireEvent.click(screen.getByRole('button'));
  expect(handleClick).not.toBeCalled(); // ❌
});

test('click on hidden button', () => {
  const handleClick = jest.fn();

  render(
    <button data-testid="button" style={{ display: 'none' }} onClick={handleClick}>
      Save
    </button>
  );

  fireEvent.click(screen.getByTestId('button'));
  expect(handleClick).not.toBeCalled(); // ❌
});

test('click on hidden button', () => {
  const handleClick = jest.fn();

  render(
    <button style={{ display: 'none' }} onClick={handleClick}>
      Save
    </button>
  );

  fireEvent.click(screen.getByRole('button')); // ❌
  expect(handleClick).not.toBeCalled();
});

width:1100px


test('bounding client rect', async () => {
  const handleClick = jest.fn();

  render(
    <button id="button" style={{ width: 100, height: 100}} onClick={handleClick}>
      Save
    </button>
  );

  const button = screen.getByRole('button');
  expect(button.getBoundingClientRect().height).toBe(100);
  expect(button.getBoundingClientRect().width).toBe(100);
});

width:900px


test('z-index', async () => {
  const handleClick = jest.fn();

  render(
    <>
      <button id="button" style={{ width: 100, height: 100}} onClick={handleClick}>
        Save
      </button>
      <div style={{position: "absolute", left: 0, right: 0, top: 0, botton: 0, zIndex: 100}}></div>
    </>
  )

  fireEvent.click(screen.getByRole('button'));
  expect(handleClick).not.toBeCalled(); // ❌
});

const ReadonlyInput = () => {
  const [value, setValue] = React.useState("");

  return (
    <input readOnly value={value} onChange={(event) => setValue(event.target.value)} />
  );
};

test('readonly input', async () => {
  render(
    <ReadonlyInput />
  );

  fireEvent.change(screen.getByRole('textbox'), {
    target: {value: "Hello"}
  });

  expect(screen.getByRole('textbox').value).toBe("");
});

width:900px


  • стили
  • реальные возможности
  • обязательные поля

Best Practice


Использование обертки

// ❌
const wrapper = render(<Example prop="1" />);
wrapper.rerender(<Example prop="2" />);
// ✅
const { rerender } = render(<Example prop="1" />);
rerender(<Example prop="2" />);
// ✅
const view = render(<Example prop="1" />);
view.rerender(<Example prop="2" />);

Демонтирование деревьев в React, смонтированных при рендере

// ❌
import {render, screen, cleanup} from '@testing-library/react';
afterEach(cleanup);
// ✅
import {render, screen} from '@testing-library/react';

// ❌
const {getByRole} = render(<Example />);
const errorMessageNode = getByRole('alert');
// ✅
import {render, screen} from '@testing-library/react';
render(<Example />);
const errorMessageNode = screen.getByRole('alert');
  • Используйте screen
    • не деструктурируйте
    • можно использовать screen.debug

const button = screen.getByRole('button', {name: /disabled button/i});
// ❌
expect(button.disabled).toBe(true);
// Сообщение об ошибке:
//  expect(received).toBe(expected) // равенство проверяется Object.is
//
//  Expected: true
//  Received: false
// ✅
expect(button).toBeDisabled();
// Сообщение об ошибке:
//   Received element is not disabled:
//     <button />

Используйте верные селекторы

<label>Username</label><input data-testid="username" />
// ❌
screen.getByTestId('username');


<label for="username">Username</label><input id="username" type="text" />
// ✅
screen.getByRole('textbox', {name: /username/i});

Не используйте контейнер

// ❌
const { container } = render(<Example />);
const button = container.querySelector('.btn-primary');
expect(button).toHaveTextContent(/click me/i);
// ✅
render(<Example />);
screen.getByRole('button', {name: /click me/i});

Не рекомендуется

  • button
  • .btn.btn-large
  • #main

Селекторы по тексту

// ❌
screen.getByTestId('submit-button');
// ✅
screen.getByRole('button', {name: /submit/i});

Выбирайте элементы по информативному названию aria-свойств, которые читают скринридеры Работает, даже если текстовое содержимое вашего элемента разбито на разные дочерние элементы

<button><span>Hello</span> <span>World</span></button>
// ❌ падает со следующей ошибкой:
// Unable to find an element with the text: /hello world/i. This could be
// because the text is broken up by multiple elements. In this case, you can
// provide a function for your text matcher to make your matcher more flexible.
screen.getByText(/hello world/i);
// ✅
screen.getByRole('button', {name: /hello world/i});

Используйте find вместо waitFor

// ❌
const submitButton = await waitFor(() =>
  screen.getByRole('button', {name: /submit/i}),
);
// ✅
const submitButton = await screen.findByRole('button', {name: /submit/i});

Побочные эффекты в waitFor

You can't use snapshot assertions within waitFor

// ❌
await waitFor(() => {
  fireEvent.keyDown(input, {key: 'ArrowDown'});
  expect(screen.getAllByRole('listitem')).toHaveLength(3);
});
// ✅
fireEvent.keyDown(input, {key: 'ArrowDown'});
await waitFor(() => {
  expect(screen.getAllByRole('listitem')).toHaveLength(3);
});

  • Используйте только query* для утверждений о том, что элемент не может быть найден
  • Используйте user-event
  • Используйте плагины для линтера для Testing Library
    • eslint-plugin-testing-library
    • eslint-plugin-jest-dom

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

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

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

Об обучении на Хекслете

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

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

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

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

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

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

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

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

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

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

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

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