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
- если очень нужно ->
Рекомендуем использовать библиотеку 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');
});
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.