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
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.