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