- React unit test
- React Testing Library
- The problem
- To solve
- What you don’t have
- React Testing Library Common Testing scenarios
- Rendering a component
- Selecting elements
- Search variants
- The difference between getBy and queryBy
- Search multiple elements
- Assertive Functions
- Fire event
- Callback handlers
- Asynchronous
- React Router
- React Redux
- React Testing Library common misuses
- Which query should be used
- Pay attention to
- Useful browser extension
- Reference
- React Testing Library
React Testing Library
The React Testing Library is built on top of the DOM Testing Library, which provides several apis for handling React Components. (If you Create a project using the React App, it already supports writing test code using the React Testing Library.)
The problem
If you want to write maintainable tests for your WEB UI. To achieve this goal, you want your tests to bypass the implementation details of the component and focus more on ensuring that it does what you expect. On the other hand, the test library should be in a long-term, maintainable state, where changing the implementation of the application without changing the functionality (i.e., code refactoring) does not require rewriting tests and slowing down the project.
To solve
The React Testing Library is a very lightweight solution for Testing the React Components. The main function it provides is to find DOM nodes in a manner similar to how users find elements on a page. This way of testing lets you make sure your Web UI is working properly. The React Testing Library’s main guidelines are:
The more your tests resemble the way your software is used, the more confidence your tests will give you
You can find form elements by Label, links and buttons by Text, and similar look-ups. It also provides the data-testid to find meaningless or impractical elements of the content or tag (I understand that a button is an icon, which cannot be described directly)
This library is a substitute for Enzyme. You can use Enzyme to follow the above rules to test, but since Enzyme provides a lot of extra functionality for implementing detailed tests for your application, forcing it makes writing tests more difficult.
What you don’t have
- Tests run the program or framework
- Specific to a test framework
React Testing Library Common Testing scenarios
Rendering a component
Here are the components to test:
import React from 'react';
const title = 'Hello React';
function App() {
return <div>{title}</div>;
}
export default App;
Copy the code
You can render a component in your test and then access it in later tests
import React from 'react';
import { render } from '@testing-library/react';
import App from './App';
describe('App'.() = > {
test('renders App component'.() = > {
render(<App />);
});
});
Copy the code
You can use screen.debug() to see what the RENDERED HTML DOM tree looks like. You can use debug to see the elements visible on the current page before you start querying for them.
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
describe('App'.() = > {
test('renders App component'.() = > {
render(<App />);
screen.debug();
});
});
Copy the code
<body>
<div>
<div>
Hello React
</div>
</div>
</body>
Copy the code
React generates an HTML DOM tree using the React features (useState, Event Handler,props, and Component). As you can see, the React Testing Library doesn’t care how the actual components are written, and renders a normal HTML DOM tree. So when we test, we only need to test the RENDERED HTML DOM tree.
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;
Copy the code
<body>
<div>
<div>
<div>
<label
for="search"
>
Search:
</label>
<input
id="search"
type="text"
value=""
/>
</div>
<p>
Searches for
...
</p>
</div>
</div>
</body>
Copy the code
The React Testing library is used to interact with the React component as users do. All the user sees is the HTML rendered from the React component, so that’s why this HTML structure is treated as output rather than two separate React components.
Selecting elements
After rendering the React component, the React Testing Library provides you with several different search methods to retrieve elements. The obtained elements can then be used for later assertions or user interactions. Here’s how to use them:
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
describe('App'.() = > {
test('renders App component'.() = > {
render(<App />);
expect(screen.getByText('Search:')).toBeInTheDocument();
});
});
Copy the code
If you are not familiar with the HTML DOM tree rendered by the component, you are advised to use debug to view the tree structure. Then use the screen object’s search method to find the element you want.
Usually,getByText will throw an error if the element is not found, which will help you to know that you did not get the element you want correctly before you proceed. Some people may also use this error-throwing feature for implicit type judgments, but it is not recommended
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
describe('App'.() = > {
test('renders App component'.() = > {
render(<App />);
// Implicit type judgment
// because getByText would throw error
// if element wouldn't be there
screen.getByText('Search:');
// Explicit type judgment
// recommended
expect(screen.getByText('Search:')).toBeInTheDocument();
});
});
Copy the code
GetByText can accept not only strings as query conditions, but also regular expressions. String arguments are used for full matches, while regular expressions are used for partial matches, which can be more convenient and flexible in some cases.
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
describe('App'.() = > {
test('renders App component'.() = > {
render(<App />);
// fails
expect(screen.getByText('Search')).toBeInTheDocument();
// succeeds
expect(screen.getByText('Search:')).toBeInTheDocument();
// succeeds
expect(screen.getByText(/Search/)).toBeInTheDocument();
});
});
Copy the code
Of course getByText is only one of many search methods, the other search methods, and the preference of the method and which query should be used see below
Search variants
In addition to the query function, there is a query variant, queryBy findBy. Specific methods are as follows:
- queryByText
- queryByRole
- queryByLabelText
- queryByPlaceholderText
- queryByAltText
- queryByDisplayValue
- findByText
- findByRole
- findByLabelText
- findByPlaceholderText
- findByAltText
- findByDisplayValue
The difference between getBy and queryBy
The biggest questions are usually: what use should you use getBy, and when should you use the other two variants queryBy findBy
If you want to determine that an element does not exist, assert it. Using getBy in this case results in a test error. This works fine using queryBy.
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
describe('App'.() = > {
test('renders App component'.() = > {
render(<App />);
screen.debug();
// fails
expect(screen.getByText(/Searches for JavaScript/)).toBeNull();
});
});
Copy the code
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
describe('App'.() = > {
test('renders App component'.() = > {
render(<App />);
expect(screen.queryByText(/Searches for JavaScript/)).toBeNull();
});
});
Copy the code
FindBy is typically used for asynchronous elements. In the following example, after the initial rendering, the component retrieves the user’s data remotely. After the data is retrieved, the component is re-rendered, and the conditional rendering part is rendered Signed in as.
function getUser() {
return Promise.resolve({ id: '1'.name: 'Robin' });
}
function App() {
const [search, setSearch] = React.useState(' ');
const [user, setUser] = React.useState(null);
React.useEffect(() = > {
const loadUser = async() = > {const user = awaitgetUser(); setUser(user); }; loadUser(); } []);function handleChange(event) {
setSearch(event.target.value);
}
return (
<div>
{user ? <p>Signed in as {user.name}</p> : null}
<Search value={search} onChange={handleChange}>
Search:
</Search>
<p>Searches for {search ? search : '... '}</p>
</div>
);
}
Copy the code
If we want to test how the page changes before and after asynchronously fetching data, we can use findBy to WaitFor the element we are updating instead of WaitFor.
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
describe('App'.() = > {
test('renders App component'.async () => {
render(<App />);
expect(screen.queryByText(/Signed in as/)).toBeNull();
expect(await screen.findByText(/Signed in as/)).toBeInTheDocument();
});
});
Copy the code
In short,getBy is used for normal query elements,queryBy is used for querying and asserting elements that we wish didn’t exist, and findBy is used for querying asynchronous elements that need to wait.
Search multiple elements
If you want to assert multiple elements, you can use the multi-element query approach
- getAllBy
- queryAllBy
- findAllBy
Assertive Functions
In addition to the usual Jest assertion functions, the React Testing Library provides several common assertion functions, similar to the toBeInTheDocument we used above.
- toBeDisabled
- toBeEnabled
- toBeEmpty
- toBeEmptyDOMElement
- toBeInTheDocument
- toBeInvalid
- toBeRequired
- toBeValid
- toBeVisible
- toContainElement
- toContainHTML
- toHaveAttribute
- toHaveClass
- toHaveFocus
- toHaveFormValues
- toHaveStyle
- toHaveTextContent
- toHaveValue
- toHaveDisplayValue
- toBeChecked
- toBePartiallyChecked
- toHaveDescription
Fire event
So far, we’ve only touched on testing if the current component renders an element. Next, user interaction:
The following test scenario is where the user enters a new value into the input, the page is re-rendered, and the new value is displayed on the page.
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import App from './App';
describe('App'.() = > {
test('renders App component'.() = > {
render(<App />);
screen.debug();
fireEvent.change(screen.getByRole('textbox'), {
target: { value: 'JavaScript'}}); screen.debug(); }); });Copy the code
Screen.debug () prints the changes in the HTML DOM tree rendered after typing the new value. You can see that the second output contains the new value.
In addition, if your component contains asynchronous tasks, such as requesting user information at the beginning of the page load, the above test code will display the following error message: “Warning: An update to App inside a test was not wrapped in act(…) “. This means that there is an asynchronous task that we need to wait for, and we need to wait for the asynchronous character to complete the task before doing any other operations
describe('App'.() = > {
test('renders App component'.async () => {
render(<App />);
// wait for the user to resolve
// needs only be used in our special case
await screen.findByText(/Signed in as/);
screen.debug();
fireEvent.change(screen.getByRole('textbox'), {
target: { value: 'JavaScript'}}); screen.debug(); }); });Copy the code
We then assert that the page changes before and after the input type event
describe('App'.() = > {
test('renders App component'.async () => {
render(<App />);
// wait for the user to resolve
// needs only be used in our special case
await screen.findByText(/Signed in as/);
expect(screen.queryByText(/Searches for JavaScript/)).toBeNull();
fireEvent.change(screen.getByRole('textbox'), {
target: { value: 'JavaScript'}}); expect(screen.getByText(/Searches for JavaScript/)).toBeInTheDocument();
});
});
Copy the code
For the test of Event, it is officially recommended to use it. For specific reasons, see the following common incorrect uses
Callback handlers
How to test callback functions: Mock callback functions passed to render components
function Search({ value, onChange, children }) {
return (
<div>
<label htmlFor="search">{children}</label>
<input
id="search"
type="text"
value={value}
onChange={onChange}
/>
</div>
);
}
Copy the code
describe('Search'.() = > {
test('calls the onChange callback handler'.() = > {
const onChange = jest.fn();
render(
<Search value="" onChange={onChange}>
Search:
</Search>
);
fireEvent.change(screen.getByRole('textbox'), {
target: { value: 'JavaScript'}}); expect(onChange).toHaveBeenCalledTimes(1);
});
});
Copy the code
Asynchronous
The following example is an example of remotely fetching data and displaying it on a page:
import React from 'react';
import axios from 'axios';
const URL = 'http://hn.algolia.com/api/v1/search';
function App() {
const [stories, setStories] = React.useState([]);
const [error, setError] = React.useState(null);
async function handleFetch(event) {
let result;
try {
result = await axios.get(`${URL}? query=React`);
setStories(result.data.hits);
} catch(error) { setError(error); }}return (
<div>
<button type="button" onClick={handleFetch}>
Fetch Stories
</button>
{error && <span>Something went wrong ...</span>}
<ul>
{stories.map((story) => (
<li key={story.objectID}>
<a href={story.url}>{story.title}</a>
</li>
))}
</ul>
</div>
);
}
export default App;
Copy the code
After clicking the button, the request begins. Here is the corresponding test code:
import React from 'react';
import axios from 'axios';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import App from './App';
jest.mock('axios');
describe('App'.() = > {
test('fetches stories from an API and displays them'.async() = > {const stories = [
{ objectID: '1'.title: 'Hello' },
{ objectID: '2'.title: 'React'},]; axios.get.mockImplementationOnce(() = >
Promise.resolve({ data: { hits: stories } })
);
render(<App />);
await userEvent.click(screen.getByRole('button'));
const items = await screen.findAllByRole('listitem');
expect(items).toHaveLength(2);
});
});
Copy the code
Before the Render component mocks the HTTP request, we need to make sure that our mock data is returned when the request is made.
Test request error code:
import React from 'react';
import axios from 'axios';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import App from './App';
jest.mock('axios');
describe('App'.() = > {
test('fetches stories from an API and displays them'.async () => {
...
});
test('fetches stories from an API and fails'.async () => {
axios.get.mockImplementationOnce(() = >
Promise.reject(new Error())); render(<App />);
await userEvent.click(screen.getByRole('button'));
const message = await screen.findByText(/Something went wrong/);
expect(message).toBeInTheDocument();
});
});
Copy the code
React Router
Test components:
// app.js
import React from 'react'
import { Link, Route, Switch, useLocation } from 'react-router-dom'
const About = () = > <div>You are on the about page</div>
const Home = () = > <div>You are home</div>
const NoMatch = () = > <div>No match</div>
export const LocationDisplay = () = > {
const location = useLocation()
return <div data-testid="location-display">{location.pathname}</div>
}
export const App = () = > (
<div>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Switch>
<Route exact path="/">
<Home />
</Route>
<Route path="/about">
<About />
</Route>
<Route>
<NoMatch />
</Route>
</Switch>
<LocationDisplay />
</div>
)
Copy the code
Test code:
// app.test.js
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { createMemoryHistory } from 'history'
import React from 'react'
import { Router } from 'react-router-dom'
import '@testing-library/jest-dom/extend-expect'
import { App, LocationDisplay } from './app'
test('full app rendering/navigating'.() = > {
const history = createMemoryHistory()
render(
<Router history={history}>
<App />
</Router>
)
// verify page content for expected route
// often you'd use a data-testid or role query, but this is also possible
expect(screen.getByText(/you are home/i)).toBeInTheDocument()
const leftClick = { button: 0 }
userEvent.click(screen.getByText(/about/i), leftClick)
// check that the content changed to the new page
expect(screen.getByText(/you are on the about page/i)).toBeInTheDocument()
})
test('landing on a bad page'.() = > {
const history = createMemoryHistory()
history.push('/some/bad/route')
render(
<Router history={history}>
<App />
</Router>
)
expect(screen.getByText(/no match/i)).toBeInTheDocument()
})
test('rendering a component that uses useLocation'.() = > {
const history = createMemoryHistory()
const route = '/some-route'
history.push(route)
render(
<Router history={history}>
<LocationDisplay />
</Router>
)
expect(screen.getByTestId('location-display')).toHaveTextContent(route)
})
Copy the code
React Redux
Test components:
import { connect } from 'react-redux'
const App = props= > {
return <div>{props.user}</div>
}
const mapStateToProps = state= > {
return state
}
export default connect(mapStateToProps)(App)
Copy the code
Test code:
// test-utils.js
import React from 'react'
import { render as rtlRender } from '@testing-library/react'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
// Import your own reducer
import reducer from '.. /reducer'
function render(ui, { initialState, store = createStore(reducer, initialState), ... renderOptions } = {}) {
function Wrapper({ children }) {
return <Provider store={store}>{children}</Provider>
}
return rtlRender(ui, { wrapper: Wrapper, ... renderOptions }) }// re-export everything
export * from '@testing-library/react'
// override render method
export { render }
Copy the code
React Testing Library common misuses
- Using cleanup
Suggestion: Don’t use cleanup
Most major testing frameworks now do automatic cleanup, so manual cleanup behavior is no longer required
// bad
import { render, screen, cleanup } from "@testing-library/react";
afterEach(cleanup)
// good
import { render,screen } from "@testing-library/react";
Copy the code
- Not using screen
Suggestion: Use screen for Querying and debugging
DOM Testing Library v6.11.0 adds Screen, which avoids manually adding and removing query functions. You just use Screen, and the editor automatically completes the rest of the query functions for you
// bad
const { getByRole } = render(<Example />);
const errorMesssageNode = getByRole("alert");
// good
render(<Example />)
const errorMessageNode = screen.getByRole("alert");
Copy the code
- Using the Wrong assertion
Suggestion: Install and use @testing-library/jest-dom
The toBedisabled assertion comes from jest-DOM. Using the jest-dom is highly recommended, as it is much better for receiving error messages
const button = screen.getByRole("button, {name: /disabled button/i}); // bad expect(button.disabled).toBe(true); // error message: // expect(received).toBe(expected) // Obejct.is equality // // Expected: true // Received: false // good expect(button).toBeDisabled() // error massage // received element id not disabled // Copy the code
- Wrapping things in act unnecessarily
Advice: Learn when act is necessary and don’t wrap things in act unnecessarily.
Render fireEvent already includes act functionality, so you don’t need act anymore
// bad
act(() = > {
render(<Example />)});const input = screen.getByRole('textbox', {name: /choose a fruit/i});
act(() = > {
fireEvent.keyDown(input, {key: 'ArrowDown'});
});
// good
render(<Example />);
const input = screen.getByRole('textbox', {name: /choose a fruit/i});
fireEvent.keyDown(input, {key: 'ArrowDown'});
Copy the code
- Using the wrong query
This is a recommended order of queries: which queries should be used, and which queries should be performed in a way that is closest to the user
// bad
// assuming you've got this DOM to work with:
// <label>Username</label><input data-testid="username" />
screen.getByTestId('username');;
// good
// change the DOM to be accessible by associating the label and setting the type
// <label for="username">Username</label><input id="username" type="text" />
screen.getByRole('textbox', {name: /username/i});
// bad
const {container} = render(<Example />);
const button = container.querySelector('.btn-primary');
expect(button).toHaveTextContent(/click me/i);
// good
render(<Example />)
screen.getByRole('button', {name: /click me/i});
// bad
screen.getByTestId('submit-button');
// good
screen.getByRole('button', {name: /submit/i});
Copy the code
- Not using @testing-library/user-event
Suggestion: Use @testing-library/user-event over fireEvent where possible.
The @testing-library/ user-Event is built on fireEvent, which provides several approaches that are more similar to user interaction. In the example below, fireEvent.change will only fire the change event above the input. But userEvent.type also fires keyDown, keyPress, and keyUp events. It is closer to the actual user interaction.
// bad
fireEvent.change(input, {target: {value: 'hello world'}});
// good
userEvent.type(input, 'hello world');
Copy the code
- Using query variants for anything except checking for non-existence*
Only use the query* variants for asserting that an element cannot be found
Query methods like queryByRole are used only to determine that an element does not exist on the current page
// bad
expect(screen.queryByRole('alert')).toBeInTheDocument();
// good
expect(screen.getByRole('alert')).toBeInTheDocument();
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
Copy the code
- Using waitFor to wait for elements that can be queried with find
The following two pieces of code are equivalent, but the second is simpler and has a better error message
// bad
const submitButton = await waitFor(() = >
screen.getByRole('button', {name: /submit/i}),)// good
const submitButton = await screen.findByRole('button', {name: /submit/i})
Copy the code
Which query should be used
As a guideline, your tests should be as similar as possible to users using your pages or components. Here is a limited order of recommendations:
- Queries Accessible to Everyone
- getByRole
It can be used to query all elements in the accessibility tree, and the returned elements can be filtered through the name option. It should be your first choice for queries. In most cases, it is used with the name option, like this: getByRole(‘button’, {name: /submit/ I}). Here is a list of Roles for reference
- getByLabelText
This is the preference for querying form elements
- getByPlaceholderText
This is an alternative to querying form elements
- getByText
It doesn’t work on forms, but it’s the first way to find most non-interactive elements like DIV and SPANS
- getByDisplayValue
2.Semantic Queries
- getByAltText
If the element you are querying supports Alttext (such as IMG Area input), you can use it to query
- getByTitle
Through the title property, but note that titile is not directly visible to a user viewing the page from a screen
3.Test IDs
- getByTestId
It is usually used to query some content that the user cannot hear or see, which cannot be matched by Role or Text.
Pay attention to
It is possible to use the querySelector DOM API to query, but this is highly undesirable because the properties are not visible to the user. If you have to, you can add testid to it, like the following
// @testing-library/react
const { container } = render(<MyComponent />)
const foo = container.querySelector('[data-foo="bar"]')
Copy the code
Useful browser extension
The extension Testing Playground can help you find the best way to query
Reference
react-testing-library
Which query should I use
Appearance and Disappearance
Considerations for fireEvent
Test for React Router
Test for React redux
Common mistakes with React Testing Library
How to use React Testing Library Tutorial