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

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

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

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

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

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