- Before
- Installation
- Test Structure
- Find the Elements
- Example
- Assertions
- FireEvents
- Mocks
- Mocking Components
- Mocking useState Functions
- Mocking APIs
- Asynchronous
- Before & After Each
- Integration Tests
- MSW (Mock Service Worker)
React-testing-library is a submodule of testing-library. You can read the documentation from the links, but I recommend watching Netninja and Laith Harb's tutorial.
If you are using create-react-app, you should already have react-testing-library
installed in your project.
npm install --save-dev @testing-library/react
There is a general formula to test everything in your react app. When you test any component, you can go through the following steps:
- Create a test block by
test()
orit()
- Render the component you want to test
- Get or query or find the elements we want to interact with
- Interact with those elements
- Assert the results are as expected
Here is an example of a test structure from AddInput.test.js
// 1.
test("should be able to type in input", () => {
// 2.
render(<MockAddInput />);
// 3.
const inputDOM = screen.getByPlaceholderText("Add a new task here...");
// 4.
fireEvent.change(inputDOM, { target: { value: "Learn React" } });
// 5.
expect(inputDOM.value).toBe("Learn React");
});
There are 3 ways to find the element we want to interact with, namely getByXXX
, queryByXXX
, and findByXXX
. All of them also have a corresponding method for finding all matching elements, i.e. getAllByXXX
, queryAllByXXX
, and findAllByXXX
. We can simply distinguish the differences between the 3 methods by the following rules:
- If we cannot find the element,
getBy
will throw an error, butqueryBy
will return null - If we cannot find any element,
queryAllBy
will return an empty array. - Only
findBy
can handle elements related to async/await - If there are multiple matching elements,
getBy
,queryBy
,findBy
will throw an error
getBy | queryBy | findBy | getAllBy | queryAllBy | findAllBy | |
---|---|---|---|---|---|---|
No match | error | null | error | error | [] | error |
1 Match | return | return | return | array | array | array |
N Matches | error | error | error | array | array | array |
Async/Await | no | no | yes | no | no | yes |
When you want to find the element to interact with, you should consider the user's experience when using these elements, and take the following order to get them.
getByRole
>getByLabelText
>getByPlaceholderText
>getByText
>getByDisplayValue
(Accessible by everyone)getByAltText
>getByTitle
(Accessible by robots)getByTestId
(No one can access)
Let's test how to find the elements on our Header.js
:
If the
title
attribute of a component is defined, then when you usegetByRole
, thename
in the options will always equal thetitle
and not thetext (children)
// Header.js
export default function Header({ title }) {
return (
<h1 title="Header" className="header" data-testid="header-1">
{title}
</h1>
);
}
These are some getBy
and getAllBy
examples:
render(<Header title="Hello" />);
const header = screen.getByText(/hello/i);
const header = screen.getByRole("heading", { name: /header/i });
const header = screen.getByTitle(/header/i);
const header = screen.getByTestId("header-1");
expect(header).toBeInTheDocument();
const headers = screen.getAllByText(/hello/i);
expect(headers).toHaveLength(1);
expect(headers.length).toBe(1);
A queryBy
example:
it("queryBy will return null for further testing", () => {
render(<Header title="Hello" />);
const header = screen.queryByText(/other/i);
expect(header).not.toBeInTheDocument();
});
A findBy
example:
it("findBy will return result after async/await", async () => {
render(<Header title="Hello" />);
const header = await screen.findByText(/hello/i);
expect(header).toBeInTheDocument();
});
When we want to validate the behavior, text, or value of our components, we can use the assertions we learned in Jest, such as toBe
, toEqual
, or toMatch
.
expect(headers.length).toBe(1);
expect(tasksCount.textContent).toBe("2 tasks left");
expect(inputDOM.value).toBe("Learn React");
Or, we can utilize some of the assertion functions that are built into react-testing-library
.
expect(header).toBeInTheDocument();
expect(screen.getByText(/followers/i)).toBeVisible();
expect(taskCount).toContainHTML("p");
expect(tasksCount).toHaveTextContent("2 tasks left");
expect(todo).toHaveClass("todo-item-active");
Sometimes we also want to interact with our components, like clicking a button or typing something into our input field. We can achieve these interaction by using fireEvent
in react-testing-library
.
import { fireEvent, render, screen } from "@testing-library/react";
const inputDOM = screen.getByPlaceholderText("Add a new task here...");
const addBtn = screen.getByText("Add");
fireEvent.change(inputDOM, { target: { value: "Learn React" } });
fireEvent.click(addBtn);
expect(inputDOM.value).toBe("");
When you test components, there are many things you can and should mock. Because these things are not what you should mainly focus on but still need to implement.
The component you are testing may need to be wrapped by another parent component. In this case, we can create a mock component that wraps the test component with the required parent component.
const MockTodoFooter = ({ taskCount }) => (
<BrowserRouter>
<TodoFooter numberOfIncompleteTasks={taskCount} />
</BrowserRouter>
);
render(<MockTodoFooter taskCount={1} />);
Sometimes we need to pass props to a component, and props may include functions such as setTodos
in useState. We can replace them by passing function mock - jest.fn()
instead.
const MockAddInput = () => (
<AddInput
todos={[]}
setTodos={jest.fn()} />;
)
render(<MockAddInput />);
Some components display content after calling some APIs. And we already know that it is a bad practice to fetch the real API while testing. So we should mock API requests when testing these components as well.
const mockResponse = {
data: {
results: [
{
name: { first: "John", last: "Doe" },
login: { username: "johndoe" },
picture: {
large: "https://randomuser.me/api/portraits/men/1.jpg",
},
},
],
},
};
const mockAxiosGet = jest.spyOn(axios, "get");
mockAxiosGet.mockResolvedValue(mockResponse);
An example of asynchronous testing with a real API.
it("should render multiple followers", async () => {
render(<MockFollowerList />);
const followers = await screen.findAllByTestId(/follower-item/i);
expect(followers.length).toBe(5);
});
An example of asynchronous testing with a mock API.
it("should render only 1 follower", async () => {
const mockAxiosGet = jest.spyOn(axios, "get");
mockAxiosGet.mockResolvedValue(mockResponse);
render(<MockFollowerList />);
const followers = await screen.findAllByTestId(/follower-item/i);
expect(followers.length).toBe(1);
expect(mockAxiosGet).toHaveBeenCalledTimes(1);
});
Same as in jest. You can set up anything in beforeEach
or beforeAll
, or dispose anything in afterEach
or afterAll
.
You can think of integration tests as a combination of multiple unit tests. During a integration test, we test whether the interaction between multiple components works as expected.
We can say that the following test is an integration test:
- Test if we can type anything into the input field.
- Test if the input field is cleared if we click the Add button.
- Test if the input is added to the todo list if we click the Add button.
const addTask = (tasks) => {
tasks.forEach((task) => {
const inputDOM = screen.getByPlaceholderText("Add a new task here...");
const addBtn = screen.getByText("Add");
fireEvent.change(inputDOM, { target: { value: task } });
fireEvent.click(addBtn);
expect(inputDOM.value).toBe("");
});
};
describe("add todo", () => {
test("add todo should be rendered in todolist", () => {
render(<MockTodo />);
addTask(["Learn React"]);
const todo = screen.getByText("Learn React");
expect(todo).toBeInTheDocument();
});
test("add multiple todos should be rendered in todolist", () => {
render(<MockTodo />);
addTask(["Learn React", "Learn Redux"]);
const todos = screen.getAllByTestId("todo");
expect(todos.length).toBe(2);
});
});
You can get a quick overview of MSW from this YouTube crash course.
According to msw official introduction, Mock Service Worker is an API mocking library that uses Service Worker API to intercept actual requests. And we can use MSW
not only in our testing phase but also in development phase.
First, we need to install msw
to our dev-dependencies from either npm
or yarn
:
npm install msw --save-dev
# or
yarn add msw --dev
Next, we can create a single directory to handle all the modules related to mocking. Here we create a src/mocks
directory and create a src/mocks/handlers.js
to have all our request handlers.
// src/mocks/handlers.js
import { rest } from 'msw'
export const handlers = [
rest.get("https://randomuser.me/api", (req, res, ctx) => {
// If the client's request is `https://randomuser.me/api?results=5`
// the `resultsCount` will be equal to 5
const resultsCount = req.url.searchParams.get("results");
return res(
ctx.status(200),
ctx.json({ results: [ ... ]})
);
}),
];
After we define all the api handlers, we need to add a new src/mocks/server.js
to intercept all the requests and return mock data to these requests.
import { setupServer } from "msw/node";
import { handlers } from "./handlers";
export const server = setupServer(...handlers);
Then we can go to setupTests.js created by create-react-app
, and start our mock server.
import { server } from "./mocks/server.js";
// Establish API mocking before all tests.
// onUnhandledRequest will return error
// when we face any request that is not being intercepted
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
// Reset any request handlers that we may add during the tests,
// so they don't affect other tests.
afterEach(() => server.resetHandlers());
// Clean up after the tests are finished.
afterAll(() => server.close());
Finally, let's move to FollowerList.test.js and see how we can test with msw
.
When we render(<MockFollowerList />)
, the api (https://randomuser.me/api/?results=5
) will be called in the useEffect()
in our <FollowerList />
. Then, our src/mocks/server.js
will intercept the api request, and return the mock data we defined in src/mocks/handlers.js
.
it("should render two followers from our msw handlers", async () => {
render(<MockFollowerList />);
const followers = await screen.findAllByTestId(/follower-item/i);
expect(followers.length).toBe(2);
expect(followers[0]).toHaveTextContent("John Doe");
expect(followers[1]).toHaveTextContent("Addison Bergeron");
});
Now, we also want to test the error scenario when our server is down. we can mock the server, request, and response directly in our test block using server.use()
:
import { rest } from "msw";
import { server } from "../../mocks/server";
it("should render nothing when fetching error", async () => {
server.use(
rest.get("https://randomuser.me/api", (req, res, ctx) => {
return res(
ctx.status(500),
ctx.json({ message: "Server Error" })
);
})
);
render(<MockFollowerList />);
const error = await screen.findByText(/server error/i);
expect(error).toBeInTheDocument();
});
The mocking functionality provided by msw
is not only convenient for testing purposes, but also benefits the development phase. We can follow Mock Service Worker / Integrate / Browser to set up msw
for development, so that we can use the mock data returned by the src/mocks/handlers
when developing our application.
- Install
mockServiceWorker
in thepublic/
folder:
npx msw init public/ --save
- Set up request intercept function in
src/mocks/browser.js
, as we did inserver.js
.
import { setupWorker } from "msw";
import { handlers } from "./handlers";
// This configures a Service Worker with the given request handlers.
export const worker = setupWorker(...handlers);
- Start the worker in your application's root
index.js
.
if (process.env.NODE_ENV === "development") {
const { worker } = require("./mocks/browser");
worker.start();
}
- Now all your requests will be intercepted, and
msw
will return mock data to your client browser.