preface
This paper follows the above How to test drive the development of React components? , I will continue to use it this time @testing-library/react To test our React application and briefly explain how to test asynchronous components.
Test contents of asynchronous components
We know that asynchronous request is mainly used to obtain data from the server. This asynchronous request may be triggered actively or (mouse) event response. This paper mainly includes two aspects:
- How do I test asynchronous requests made during the componentDidMount lifecycle?
- How do I test asynchronous requests from (mouse) events?
For asynchronous components, there are two steps to test:
First: test whether the asynchronous method itself has been called and passed the correct parameters.
Second: after the call, the application should respond.
Let's see how to implement it in the code?
Suppose you have a small blog application written in React. There is a login page and an article list page. The content is just like mine Blog Same.
Login test
Let's implement the login page first and make up an effect picture first

Let's write down the test cases first
- The interface contains an account and password input box
- The interface request contains username and password
- Prevent repeated clicks during login
- Login success jump page
- Login failed. Error message is displayed
Test rendering
If the code is not moved, test first to ensure that our components can be rendered.
import React from "react"; import { render } from "@testing-library/react"; import Login from "./index"; describe("Login component", () => { it("should render", () => { const { getByText } = render(<Login onSubmit={() => {}} />); expect(getByText(/account number/)).toBeInTheDocument(); expect(getByText(/password/)).toBeInTheDocument(); }); });
Login component implementation
To ensure that it is a pure component, the submission method onSubmit is passed in as a props. Next, we implement the component code
import React from "react"; function Login({ onSubmit }) { function handleSubmit(event) { event.preventDefault(); const { username, password } = event.target.elements; onSubmit({ username: username.value, password: password.value }); } return ( <form onSubmit={handleSubmit}> <div> <label htmlFor="username-field">account number:</label> <input id="username-field" name="username" type="text" /> </div> <div> <label htmlFor="password-field">password:</label> <input id="password-field" name="password" type="password" /> </div> <div> <button type="submit">Sign in</button> </div> </form> ); } export default Login;
In order to facilitate understanding, we do not use other third-party libraries. I recommend using them in the production environment react-hook-form
Test submission
Next, test that the onSubmit method must contain username and password,
We need to simulate user input. At this time, we need to install @ test library / user event
import React from "react"; import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import Login from "./index"; test("onSubmit calls with username and password", () => { let submittedData; const handleSubmit = data => (submittedData = data); render( <Login onSubmit={handleSubmit} /> ); const username = "user"; const password = "password"; userEvent.type(screen.getByPlaceholderText(/username/i), username); userEvent.type(screen.getByPlaceholderText(/password/i), password); userEvent.click(screen.getByRole("button", { name: /Sign in/ })); expect(submittedData).toEqual({ username, password }); });
We can use the get*By * function of @ testing library to get the elements in the dom. Here, we use getByPlaceholderText

The above test cases only test the login function, but we do not write the logic of login success or failure. Next, we simulate login through jest's mock function.
Test login succeeded
Since the successful test login example already includes the functions of "Test submission" and "test rendering", the first two unit tests can be deleted. After logging in, the button changes to loading status disabled.
const fakeData = { username: "username", password: "username", }; test("onSubmit success", async () => { // mock login function const login = jest.fn().mockResolvedValueOnce({ data: { success: true } }); render(<Login onSubmit={login} />); userEvent.type(screen.getByPlaceholderText(/username/i), fakeData.username); userEvent.type(screen.getByPlaceholderText(/password/i), fakeData.password); userEvent.click(screen.getByRole("button", { name: /Sign in/ })); //After logging in, the button changes to loading status disabled const button = screen.getByRole("button", { name: /loading/ }); expect(button).toBeInTheDocument(); expect(button).toBeDisabled(); await waitFor(() => expect(login).toHaveBeenCalledWith(fakeData)); expect(login).toHaveBeenCalledTimes(1); // There is no loading button in the document expect( screen.queryByRole("button", { name: "loading" }) ).not.toBeInTheDocument(); });
Next, we modify the component and write the logic of login failure. The login failure displays the return information of the server under the login box.
import React, { useState } from "react"; function Login({ onSubmit }) { const [loading, setLoading] = useState(false); const [message, setMessage] = useState(""); function handleSubmit(event) { event.preventDefault(); const { username, password } = event.target.elements; setLoading(true); onSubmit({ username: username.value, password: password.value, }) .then((res) => { setLoading(false); }) .catch((res) => { setLoading(false); setMessage(res.data.message); }); } return ( <form onSubmit={handleSubmit}> <div> <label htmlFor="username-field">account number:</label> <input id="username-field" name="username" placeholder="username" type="text" /> </div> <div> <label htmlFor="password-field">password:</label> <input id="password-field" name="password" placeholder="password" type="password" /> </div> <div> <button disabled={loading} type="submit"> {loading ? "loading" : "Sign in"} </button> </div> <div>{message}</div> </form> ); } export default Login;
Test login failed
We directly copy the successful test cases and modify the failed logic. Test case:
- After failure, the server message is displayed in the document
- After failure, the button displays login and can be clicked
test("onSubmit failures", async () => { const message = "Wrong account or password!"; // mock login function failed const login = jest.fn().mockRejectedValueOnce({ data: { message }, }); render(<Login onSubmit={login} />); userEvent.type(screen.getByPlaceholderText(/username/i), fakeData.username); userEvent.type(screen.getByPlaceholderText(/password/i), fakeData.password); userEvent.click(screen.getByRole("button", { name: /Sign in/ })); const button = screen.getByRole("button", { name: /loading/ }); expect(button).toBeInTheDocument(); expect(button).toBeDisabled(); await waitFor(() => expect(login).toHaveBeenCalledWith(fakeData)); expect(login).toHaveBeenCalledTimes(1); // Ensure that there is a message returned by the server in the document expect(screen.getByText(message)).toBeInTheDocument(); // There is no loading button in the document expect( screen.queryByRole("button", { name: "loading" }) ).not.toBeInTheDocument(); expect(screen.getByRole("button", { name: /Sign in/ })).not.toBeDisabled(); });
Blog list test
I believe that after the login test, it is not difficult for us to write the test of blog list. Let's write the test case first:
- loading is displayed in the interface request page
- The request succeeded in displaying the blog list
- If the list is empty, no data will be displayed
- The server error is displayed when the request fails
Blog list code
In the following code, react use is used. First, we need to install this package
import React from "react"; import { fetchPosts } from "./api/posts"; import { useAsync } from "react-use"; export default function Posts() { const posts = useAsync(fetchPosts, []); if (posts.loading) return "Loading..."; if (posts.error) return "Server error"; if (posts.value && posts.value.length===0) return "No data"; return ( <> <h1>My Posts</h1> <ul> {posts.value.map((post) => ( <li key={post.id}> <a href={post.url}>{post.title}</a> </li> ))} </ul> </> ); }
Of course, we can also write the implementation method of class. The code is as follows:
import React from "react"; import { fetchPosts } from "./api/posts"; export default class Posts extends React.Component { state = { loading: true, error: null, posts: null }; async componentDidMount() { try { this.setState({ loading: true }); const posts = await fetchPosts(); this.setState({ loading: false, posts }); } catch (error) { this.setState({ loading: false, error }); } } render() { if (this.state.loading) return "Loading..."; if (this.state.error) return "Server error"; if (this.state.posts && this.state.posts.length===0) return "No data"; return ( <> <h1>My Posts</h1> <ul> {this.state.posts.map((post) => ( <li key={post.id}> <a href={post.url}>{post.title}</a> </li> ))} </ul> </> ); } }
Mock interface
jest.mock("./api/posts");
We can read about it in the official documents jest.mock For more information.
All it does is tell Jest to replace the / api/posts module.
Now that we have mock, let's render the component, and the interface shows loading:
import React from "react"; import { render, screen } from "@testing-library/react"; import Post from "./index"; jest.mock("./api/posts"); test("should show loading", () => { render(<Posts />); expect(screen.getByText("Loading...")).toBeInTheDocument(); });
This is the first step. Now we need to ensure that our fetchPosts method is called correctly:
import React from "react"; import { render, screen } from "@testing-library/react"; import Posts from "./index"; import { fetchPosts } from "./api/posts"; jest.mock("./api/posts"); test("should show a list of posts", () => { render(<Posts />); expect(screen.getByText("Loading...")).toBeInTheDocument(); expect(fetchPosts).toHaveBeenCalledTimes(1); expect(fetchPosts).toHaveBeenCalledWith(); });
Test the number of calls through toHaveBeenCalledTimes, and test the parameters of the call method through toHaveBeenCalledWith. Although there is empty data here, we can also write it to ensure that the call parameters are empty.
At this point, our test code will report an error because we do not have the mock fetchposts method
import React from "react"; import { render, screen, wait } from "@testing-library/react"; import Posts from "./index"; import { fetchPosts } from "./api/posts"; jest.mock("./api/posts"); test("should show a list of posts", async () => { // mock interface const posts = [{ id: 1, title: "My post", url: "/1" }]; fetchPosts.mockResolvedValueOnce(posts); render(<Posts />); expect(screen.getByText("Loading...")).toBeInTheDocument(); expect(fetchPosts).toHaveBeenCalledWith(); expect(fetchPosts).toHaveBeenCalledTimes(1); //Wait for the title to render await waitFor(() => screen.findByText("My Posts")); posts.forEach((post) => expect(screen.getByText(post.title)).toBeInTheDocument() ); });
We use mockResolvedValueOnce to return some false data. Then, we wait for the asynchronous method to resolve and wait for the Posts component to re render. To do this, we use the waitFor method to check whether the title is rendered at the same time, and then traverse the check to ensure that each title is on the page.
Test interface error
Next, we will test whether the error is correctly rendered, so we only need to modify mock:
test("should show an error message on failures", async () => { fetchPosts.mockRejectedValueOnce("Error!"); render(<Posts />); expect(screen.getByText("Loading...")).toBeInTheDocument(); await waitFor(() => { expect(screen.getByText("Server error")).toBeInTheDocument(); }); expect(fetchPosts).toHaveBeenCalledWith(); expect(fetchPosts).toHaveBeenCalledTimes(1); });
Summary
Here are the steps to test asynchronous components:
- mock {enables the component to obtain static false data;
- Test loading status;
- Test whether the asynchronous method is called correctly with correct parameters;
- Test whether the component renders the data correctly
- When testing for asynchronous method errors, does the component render the correct state
The page Jump is not tested after successful login, so how to test the react route? Please pay attention to me and I will publish the following of React test series as soon as possible.
I hope this article is helpful to you. You can also refer to my previous articles or exchange your ideas and experiences in the comment area. Welcome to explore the front end together.