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