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

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

Видео может быть заблокировано из-за расширений браузера. В статье вы найдете решение этой проблемы.

React Testing Library

Hexlet


React Testing Library

  • Ставится поверх DOM Testing Library добавляя API для работы с React компонентами
  • То есть ваши тесты будут работать не с экземплярами отрендеренных React-компонентов, а с реальными DOM узлами

Цели

  • Поддерживаемые тесты
  • Уверенность в тестах
  • Избегайте проверок деталей реализации
    • Внутреннее состояние компонента
    • Внутренние методы компонента
    • Методы жизненного цикла компонента
    • Дочерние компоненты
  • Долгоиграющие тесты
    • Рефакторинг не ломает ваши тесты

// import react-testing methods
import { render } from '@testing-library/react'

// импорт тестируемого компонента
import Button from '../Button'

test('button', async () => {
  // Выбираем
  // Действуем
  // Проверяем
})

Методы API


render

import { render } from '@testing-library/react'

render(<div />)
render(<App />)

test('renders personalized greeting', async () => {
  const { getByText } = render(<HelloMessage name="Ruslan" />)

  await waitForElement(() => getByText(/hello ruslan/i))
})

test('renders a message', () => {
  const { container, getByText, rerender, unmount } = render(<Greeting name="Ivan" />)
  expect(getByText('Hello, Ivan!')).toBeInTheDocument()
  expect(container.firstChild).toMatchInlineSnapshot(`
    <h1>Hello, Ivan!</h1>
  `)

  // ре-рендер того же компонента с другими пропсами
  rerender(<Greeting name="Ruslan" />)
  expect(getByText('Hello, Ruslan!')).toBeInTheDocument()

  unmount()
})

import { render, screen } from '@testing-library/react'
import App from './App'

describe('App', () => {
  test('renders App component', () => {
    render(<App />)

    screen.debug() // выведет весь DOM
  })
})

  • Замена библиотеке Enzyme
    • Концептуальное отличие
    • НЕ поддерживает shallow rendering
  • На RTL невозможно мигрировать с Enzyme
  • Избегайте мока компонентов
    • если очень нужно -> jest.fn

width:1100px


Рекомендуем использовать библиотеку Mock Service Worker library для декларативного мока взаимодействия с API в ваших тестах вместо того, чтобы делать стаб window.fetch.


// объявляем какой запрос к API мокать
const server = setupServer(
  rest.get('/users', (req, res, ctx) => {
    return res(ctx.json({ users: [...] }));
  });
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

// ...  

test('filter users list', async () => {
  // ...
});

Events


test('change values via the fireEvent.change method', () => {
  const handleChange = jest.fn()
  const { container } = render(<input type="text" onChange={handleChange} />)
  const input = container.firstChild
  fireEvent.change(input, { target: { value: 'a' } })
  expect(handleChange).toHaveBeenCalledTimes(1)
  expect(input.value).toBe('a')
})

test('change values via the fireEvent.change method', () => {
  const handleChange = jest.fn()
  const { findByRole } = render(<input type="text" onChange={handleChange} />)
  const input = findByRole('textbox')
  fireEvent.change(input, { target: { value: 'a' } })
  expect(handleChange).toHaveBeenCalledTimes(1)
  expect(input.value).toBe('a')
})


test('checkboxes (and radios) must use fireEvent.click', () => {
  const handleChange = jest.fn();
  const { findByRole } = render(;
    <input type="checkbox" onChange={handleChange} />
  );
  const checkbox = findByRole('checkbox');
  fireEvent.click(checkbox);
  expect(handleChange).toHaveBeenCalledTimes(1);
  expect(checkbox.checked).toBe(true);
});


const Button = ({ onClick, children }) => (
  <button onClick={onClick}>{children}</button>
)
import { render, screen, fireEvent } from '@testing-library/react'
import Button from '../Button'

test('calls onClick prop when clicked', () => {
  const handleClick = jest.fn()
  render(<Button onClick={handleClick}>Click Me</Button>)
  fireEvent.click(screen.getByText(/click me/i))
  expect(handleClick).toHaveBeenCalledTimes(1)
})

React Testing Library не особо заботят реальные компоненты


import React from 'react'

function App() {
  const [search, setSearch] = React.useState('')

  function handleChange(event) {
    setSearch(event.target.value)
  }

  return (
    <div>
      <Search value={search} onChange={handleChange}>
        Search:
      </Search>

      <p>
        Searches for
        {search ? search : '...'}
      </p>
    </div>
  )
}

function Search({ value, onChange, children }) {
  return (
    <div>
      <label htmlFor="search">{children}</label>
      <input
        id="search"
        type="text"
        value={value}
        onChange={onChange}
      />
    </div>
  )
}

export default App

<body>
  <div>
    <div>
      <div>
        <label for="search">Search:</label>
        <input id="search" type="text" value="" />
      </div>
      <p>
        Searches for
        ...
      </p>
    </div>
  </div>
</body>

// Выбираем
render(<Fetch url="/user" />)

// Действуем
fireEvent.click(screen.getByText('Load User'))

await waitFor(() =>
  // getByRole выбросит ошибку, если не найдет компонент
  screen.getByRole('heading'),
)

// Проверяем
expect(screen.getByRole('alert')).toHaveTextContent('Oops, failed to fetch!')
expect(screen.getByRole('button')).not.toBeDisabled()

// Redux
const renderComponent = ({ count }) =>
  render(
    <Provider store={createStore(counterReducer, { count })}>
      <ReduxCounter />
    </Provider>,
  )

test('renders initial count', async () => {
  // рендерим новый экземпляр каждый раз чтобы избежать утечки стейта
  const { getByText } = renderComponent({ count: 5 })

  await waitForElement(() => getByText(/clicked 5 times/i))
})

test('increments count', async () => {
  // рендерим новый экземпляр каждый раз чтобы избежать утечки стейта
  const { getByText } = renderComponent({ count: 5 })

  fireEvent.click(getByText('+1'))
  await waitForElement(() => getByText(/clicked 6 times/i))
})

import { render } from '@testing-library/react'

function App() {
  const [text, setText] = React.useState('start')

  React.useEffect(() => {
    setTimeout(() => {
      setText('finish')
    }, 500)
  }, [text])

  return (
    <div>
      <h1>{text}</h1>
    </div>
  )
}

test('render app', async () => {
  const { getByText, findByText } = render(<App />)

  getByText('start')
  await findByText('finish')
})

import { render, waitForElementToBeRemoved } from '@testing-library/react'

function App() {
  const [show, setShow] = React.useState(true)

  React.useEffect(() => {
    setTimeout(() => {
      setShow(false)
    }, 500)
  }, [])

  return <div>{show ? <h1>hello</h1> : null}</div>
}

test('render app', async () => {
  const { getByText } = render(<App />)

  await waitForElementToBeRemoved(() => getByText('hello'))
})

test('validation unique', async () => {
  nock('https://cors-anywhere.herokuapp.com')
    .get(`/${rssUrl}`)
    .reply(200, rssData)

  render(html)
  userEvent.type(elements.input, rssUrl)
  userEvent.click(elements.submit)

  await waitFor(() => {
    expect(screen.getByText(/RSS has been loaded/i)).toBeInTheDocument()
  })

  userEvent.type(elements.input, rssUrl)
  userEvent.click(elements.submit)

  await waitFor(() => {
    expect(screen.getByText(/RSS already exists/i)).toBeInTheDocument()
  })
})

import { render, fireEvent } from '@testing-library/?????'

import Comp from '../Comp'

test('shows proper heading when rendered', () => {
  const { getByText } = render(Comp, { name: 'World' })

  expect(getByText('Hello World!')).toBeInTheDocument()
})

// Важно: это асинхронный тест, т.к. мы используем `fireEvent`
test('changes button text on click', async () => {
  const { getByText } = render(Comp, { name: 'World' })
  const button = getByText('Button')

  await fireEvent.click(button)

  expect(button).toHaveTextContent('Button Clicked')
})

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

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

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

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

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

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

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

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