react-testing-library
Simple and complete React DOM testing utilities that encourage good testing...
README
React Testing Library
Simple and complete React DOM testing utilities that encourage good testingpractices.
Table of Contents
The problem
The solution
[The more your tests resemble the way your software is used, the more
confidence they can give you.][guiding-principle]
Installation
- ```
- npm install --save-dev @testing-library/react
- ```
- ```
- yarn add --dev @testing-library/react
- ```
- ```
- npm install --save-dev @testing-library/react@12
- yarn add --dev @testing-library/react@12
- ```
[Docs](https://testing-library.com/react)
Suppressing unnecessary warnings on React DOM 16.8
- ```
- Warning: An update to ComponentName inside a test was not wrapped in act(...).
- ```
- ``` js
- // this is just a little hack to silence a warning that we'll get until we
- // upgrade to 16.9. See also: https://github.com/facebook/react/pull/14853
- const originalError = console.error
- beforeAll(() => {
- console.error = (...args) => {
- if (/Warning.*not wrapped in act/.test(args[0])) {
- return
- }
- originalError.call(console, ...args)
- }
- })
- afterAll(() => {
- console.error = originalError
- })
- ```
Examples
Basic Example
- ``` js
- // hidden-message.js
- import * as React from 'react'
- // NOTE: React Testing Library works well with React Hooks and classes.
- // Your tests will be the same regardless of how you write your components.
- function HiddenMessage({children}) {
- const [showMessage, setShowMessage] = React.useState(false)
- return (
- <div>
- <label htmlFor="toggle">Show Message</label>
- <input
- id="toggle"
- type="checkbox"
- onChange={e => setShowMessage(e.target.checked)}
- checked={showMessage}
- />
- {showMessage ? children : null}
- </div>
- )
- }
- export default HiddenMessage
- ```
- ``` js
- // __tests__/hidden-message.js
- // these imports are something you'd normally configure Jest to import for you
- // automatically. Learn more in the setup docs: https://testing-library.com/docs/react-testing-library/setup#cleanup
- import '@testing-library/jest-dom'
- // NOTE: jest-dom adds handy assertions to Jest and is recommended, but not required
- import * as React from 'react'
- import {render, fireEvent, screen} from '@testing-library/react'
- import HiddenMessage from '../hidden-message'
- test('shows the children when the checkbox is checked', () => {
- const testMessage = 'Test Message'
- render(<HiddenMessage>{testMessage}</HiddenMessage>)
- // query* functions will return the element or null if it cannot be found
- // get* functions will return the element or throw an error if it cannot be found
- expect(screen.queryByText(testMessage)).toBeNull()
- // the queries can accept a regex to make your selectors more resilient to content tweaks and changes.
- fireEvent.click(screen.getByLabelText(/show/i))
- // .toBeInTheDocument() is an assertion that comes from jest-dom
- // otherwise you could use .toBeDefined()
- expect(screen.getByText(testMessage)).toBeInTheDocument()
- })
- ```
Complex Example
- ``` js
- // login.js
- import * as React from 'react'
- function Login() {
- const [state, setState] = React.useReducer((s, a) => ({...s, ...a}), {
- resolved: false,
- loading: false,
- error: null,
- })
- function handleSubmit(event) {
- event.preventDefault()
- const {usernameInput, passwordInput} = event.target.elements
- setState({loading: true, resolved: false, error: null})
- window
- .fetch('/api/login', {
- method: 'POST',
- headers: {'Content-Type': 'application/json'},
- body: JSON.stringify({
- username: usernameInput.value,
- password: passwordInput.value,
- }),
- })
- .then(r => r.json().then(data => (r.ok ? data : Promise.reject(data))))
- .then(
- user => {
- setState({loading: false, resolved: true, error: null})
- window.localStorage.setItem('token', user.token)
- },
- error => {
- setState({loading: false, resolved: false, error: error.message})
- },
- )
- }
- return (
- <div>
- <form onSubmit={handleSubmit}>
- <div>
- <label htmlFor="usernameInput">Username</label>
- <input id="usernameInput" />
- </div>
- <div>
- <label htmlFor="passwordInput">Password</label>
- <input id="passwordInput" type="password" />
- </div>
- <button type="submit">Submit{state.loading ? '...' : null}</button>
- </form>
- {state.error ? <div role="alert">{state.error}</div> : null}
- {state.resolved ? (
- <div role="alert">Congrats! You're signed in!</div>
- ) : null}
- </div>
- )
- }
- export default Login
- ```
- ``` js
- // __tests__/login.js
- // again, these first two imports are something you'd normally handle in
- // your testing framework configuration rather than importing them in every file.
- import '@testing-library/jest-dom'
- import * as React from 'react'
- // import API mocking utilities from Mock Service Worker.
- import {rest} from 'msw'
- import {setupServer} from 'msw/node'
- // import testing utilities
- import {render, fireEvent, screen} from '@testing-library/react'
- import Login from '../login'
- const fakeUserResponse = {token: 'fake_user_token'}
- const server = setupServer(
- rest.post('/api/login', (req, res, ctx) => {
- return res(ctx.json(fakeUserResponse))
- }),
- )
- beforeAll(() => server.listen())
- afterEach(() => {
- server.resetHandlers()
- window.localStorage.removeItem('token')
- })
- afterAll(() => server.close())
- test('allows the user to login successfully', async () => {
- render(<Login />)
- // fill out the form
- fireEvent.change(screen.getByLabelText(/username/i), {
- target: {value: 'chuck'},
- })
- fireEvent.change(screen.getByLabelText(/password/i), {
- target: {value: 'norris'},
- })
- fireEvent.click(screen.getByText(/submit/i))
- // just like a manual tester, we'll instruct our test to wait for the alert
- // to show up before continuing with our assertions.
- const alert = await screen.findByRole('alert')
- // .toHaveTextContent() comes from jest-dom's assertions
- // otherwise you could use expect(alert.textContent).toMatch(/congrats/i)
- // but jest-dom will give you better error messages which is why it's recommended
- expect(alert).toHaveTextContent(/congrats/i)
- expect(window.localStorage.getItem('token')).toEqual(fakeUserResponse.token)
- })
- test('handles server exceptions', async () => {
- // mock the server error response for this test suite only.
- server.use(
- rest.post('/api/login', (req, res, ctx) => {
- return res(ctx.status(500), ctx.json({message: 'Internal server error'}))
- }),
- )
- render(<Login />)
- // fill out the form
- fireEvent.change(screen.getByLabelText(/username/i), {
- target: {value: 'chuck'},
- })
- fireEvent.change(screen.getByLabelText(/password/i), {
- target: {value: 'norris'},
- })
- fireEvent.click(screen.getByText(/submit/i))
- // wait for the error message
- const alert = await screen.findByRole('alert')
- expect(alert).toHaveTextContent(/internal server error/i)
- expect(window.localStorage.getItem('token')).toBeNull()
- })
- ```
We recommend using Mock Service Worker library
to declaratively mock API communication in your tests instead of stubbing
window.fetch, or relying on third-party adapters.
More Examples
We're in the process of moving examples to the
Hooks
NOTE: it is not recommended to test single-use custom hooks in isolation from
the components where it's being used. It's better to test the component that's
using the hook rather than the hook itself. The React Hooks Testing Library
is intended to be used for reusable hooks/libraries.
Guiding Principles
[The more your tests resemble the way your software is used, the more
confidence they can give you.][guiding-principle]