Зарегистрируйтесь, чтобы продолжить обучение

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