A Comprehensive Guide into Writing Unit tests in React using Jest and React Testing Library
Testing a single unit of code, without any external dependencies.
This unit could be a function, a class, or a component.
Testing the integration/interaction between two or more units of code.
For example:
- integration parent and child components
- integration between component and API service
- integration between backend service and database
Integration testing can also be categorised into the following types:
We don’t care about the implementation detail of the dependency; we only test the interaction/integration between two units.
export const TestComponent = () => {
const [loading, setLoading] = useState();
const [result, setResult] = useState();
const [showError, setShowError] = useState();
useEffect(() => {
setLoading(true);
api
.getResult()
.then((result) => setResult(result))
.catch((err) => setShowError(true));
}, []);
return (
<div>
{loading ? (
<h4>Loading...</h4>
) : showError ? (
<span>Sorry an error occured. Please try again</span>
) : (
<h2>The result is: {result}</h2>
)}
</div>
);
};
For the previous code example: we test that TestComponent
calls api.getResult()
, and how the component reacts when the service call succeeds/fails.
i.e: Error appears, success message appears, loading appears...etc.
But we don’t get into the details of how getResult
is implemented.
Testing a unit of code and its dependency’s behavior.
For the same example above:
In Deep integration testing; We would test the behavior of function getResult
and its implementation details.
Testing a live running Application, starting with the UI until DB operations.
We make sure an entire flow is working as designed.
For example:
- registering a new user (starting from registration from until it shows up in the list of users) E2E also works works with real APIs and real UI components. i.e: we don't mock the API or child components.
For the sake of this article, we will be focusing on unit testing, and shallow integration testing.
In this article we are using Jest and React Testing Library
They are recommended by React documentation, and there is a huge community and a very good documentation for both.
Lets install latest version of both:
npm i jest
npm i @testing-library/react
Most of our units have dependencies. For example:
- calling a different function
- calling an API service
- having a nested Child component ...etc
In order to test our unit of code separately without those dependencies, we use mocking.
Mocking is simply creating a copy/clone of the dependency with the same signature, that will behave any way we want it to.
That means that whenever the dependency is being called/invoked in the code, the mocked copy will be used.
The Simplest method is to create __mocks___
folder next to the module we want to mock.
For example:
models
user.js
__mocks__
user.js
jest.mock('axios')
This will mock the entire axios module, so we wont make any real API calls
However we need to define some functions otherwise when axios.get
is called, we will get an error because get will be undefined
So we need to define the methods that will be used from the mocked module as follows:
jest.mock("axios", () => {
return {
get: jest.fn(() => {}),
};
});
If we only want to mock one function from a module, but keep the rest of the functions unmocked, then we can use the original module:
jest.mock("recoil", () => {
const originalModule = jest.requireActual("recoil");
return {
...originalModule,
useRecoilValue: jest.fn(() => ({})),
};
});
Here we are only mocking useRecoilValue
from recoil.
Note: the 'jest.mock' method must be called outside of any tests, it will be executed before running any test
This method can be used for each test, when we want to change the behavior of a function based on the current test.
It allows us to change the implementation, return value, resolved or rejected values or a promise...etc
jest.spyOn(recoil, "useRecoilValue").mockReturnValueOnce({
username: oldUsername,
});
So after we start writing tests, we want to be able to measure how good our tests are.
Coverage report gives us metrics about the code that was executed, while running our tests.
So it tells us if we missed a few lines of code while testing and so on.
Here is an example of coverage report metrics:
- how many lines of code
- how many branches ( if conditions, switch cases, loops ...etc) did we cover
- how many functions
High Code Coverage doesn't ensure quality Sometimes developers can write a test that executes all lines of code but simply does nothing!
it("should display No Alert! when rendered if message is undefined", () => {
//Arrange
//Act
render(<Alert />);
//Assert
expect(true).toBeTruthy();
});
this example will pass and give a 100% code coverage, however it doesn't really test anything. So it is important to rely on coverage but it doesn't eleminate the need for code review and writing good tests.
Flaky tests are defined as tests that return both passes and failures despite no changes to the code or the test itself. similar to the example mentioned here:
it("should display No Alert! when rendered if message is undefined", () => {
//Arrange
//Act
render(<Alert />);
//Assert
expect(true).toBeTruthy();
});
Organization is important for writing good tests.
It is recommended to write tests in a separate file and name the test file with the same name as the unit under test.
For example:
For Login.jsx
, we would name the test file Login.spec.jsx
or Login.test.jsx
It is recommended to keep the test file as close to the unit under test as possible to make the relative imports easier and more readable.
We can either place them in the same folder or create a separate __tests__
folder for the tests.
For example:
Login
Login.jsx
__tests__
Login.spec.jsx
or
Login
Login.jsx
Login.spec.jsx
Note: It is not recommended to put all the tests of the Application into a separate tests folder, as it will be longer path for imports.
A descriptive name for the test is as important as the test itself.
It’s important that the name of the test describes what it is doing so whoever reads the name doesn’t need to drill into the name.
There are too many recommended ways to name tests (you can find them referenced below), but all of them agree that a name should mention ‘state under test’ and ‘expected behavior’.
For example:
‘Should throw exception when age is null’
‘Should render icon when loaded’
There is a famous strategy when writing tests, which is to divide the test into three parts
Arrange, Act, Assert
- In the Arrange part we will do all our setup; define variables, spy on functions…etc
- In the Act part we will do the action; call a function, submit a form, click a button.
- In the Assert part we will do our expect calls; expect a text to be in the document, expect result to equal a value …etc
Sometimes we want to execute the same test, but for different input/output combinations.
we can use test.each
which allows us to execute the same test for diferent parameters.
Here is an example:
instead of writing tests like the following
it("should have className alert-success when the type = success", () => {
//Arrange
//Act
render(<Alert message="test" type="success" />);
//Assert
expect(screen.getByRole("alert")).toHaveAttribute("class", "alert-success");
});
it("should have className alert-error when the type = error", () => {
//Arrange
//Act
render(<Alert message="test" type="error" />);
//Assert
expect(screen.getByRole("alert")).toHaveAttribute("class", "alert-error");
});
we can write it as
it.each([
["alert-success", "success"],
["alert-error", "error"],
])("should have className %p when the type = %p", (expectedClassname, type) => {
//Arrange
//Act
render(<Alert message="test" type={type} />);
//Assert
expect(screen.getByRole("alert")).toHaveAttribute("class", expectedClassname);
});
Always try to fail a test after it succeeds to make sure it is properly working and not affected by async code and to make sure it is not a flaky test.
Lets take an example to write unit tests for the following simple function:
export const formatString = (abc = "") => {
try {
if (!abc) return "";
return abc.replace(" ", "\n").toLowerCase();
} catch (e) {
return "";
}
};
- Start by brainstorming all the tests you are planning to write covering all the branches and if conditions
- should return empty string when input string is null
- should return empty string when input string is undefined
- should return empty string when no input string is passed as arguemnt
- should replace all spaces with new lines in the input string
- should lowercase all the words in the input string
- should return empty string if there is any error thrown
- Divide each test into three As (Arrange, Act, Assert)
it("should return empty string when input string is null", () => {
//Arrange
//Act
//Assert
});
- Write down the logic of each Area
it("should return empty string when input is null", () => {
//Arrange
const inputString = null;
const expectedString = "";
//Act
const result = formatString(inputString);
//Assert
expect(result).toEqual(expectedString);
});
- Make sure the test succeeds
- Try to change the expected value so the test fails, to make sure it is a valid test
it("should return empty string when input is null", () => {
//Arrange
const inputString = null;
const expectedString = "invalid";
//Act
const result = formatString(inputString);
//Assert
expect(result).toEqual(expectedString);
});
Lets try a more complex example for the following function that has an external dependency:
export const sendEmail = (email: string, content: string) => {
if (!email) throw new Error("email is required");
if (!content) throw new Error("content is required");
if (!email.match(/.+\@.+\..+/g))
throw new Error("email must match required format");
return sendEmailRequest(email, content).catch((err) => {
log(err);
throw new Error(err);
});
};
We want to make sure that we do Shallow Integration Testing so we don't want to test the implementation details of sendEmailRequest
function.
We only want to test how our function interacts with it.
- Since we dont want to really call the external dependency, so we need to mock it:
jest.spyOn(api, "sendEmailRequest").mockImplementationOnce(() => {});
we can mock the return value differently per test:
jest.spyOn(api, "sendEmailRequest").mockResolvedValue(true);
jest.spyOn(api, "sendEmailRequest").mockRejectedValueOnce({});
- We need to make sure to test that the external dependency is being invoked:
it("should invoke sendEmailRequest with valid parameters", () => {
//Arrange
const sendEmailRequestSpy = jest.spyOn(api, "sendEmailRequest");
//Act
sendEmail("email@email.com", "content");
//Assert
expect(sendEmailRequestSpy).toBeCalledWith("email@email.com", "content");
});
- We also need to test how our function behaves for different expected return values from dependency
it("should return the value from sendEmailRequest when it is successful", async () => {
//Arrange
jest.spyOn(api, "sendEmailRequest").mockResolvedValue("email is successful");
//Act
const result = await sendEmail("email@email.com", "content");
//Assert
expect(result).toEqual("email is successful");
});
Testing Components is pretty similar to Functions, however, most of the complexity comes from setup, dependencies, and sideEffects.
Lets start with a simple component without any dependencies:
export const Alert = ({ type, message }) => {
return (
<span
role="alert"
className={type === "success" ? "alert-success" : "alert-error"}
>
{message ?? "No Alert!"}
</span>
);
};
- We follow the same steps for writing a function but we use RTL's
render
function
import { render } from "@testing-library/react";
render(<Alert message="test" />);
- Use methods provided by RTL that makes selecting elements much easier
it("should display the input message when rendered", () => {
//Arrange
//Act
render(<Alert message="test" />);
//Assert
expect(screen.getByRole("alert")).toHaveTextContent("test");
});
it("should display No Alert! when rendered if message is undefined", () => {
//Arrange
//Act
render(<Alert />);
//Assert
screen.getByText("No Alert!"); // this will throw an error if element is not found so the test will fail
});
it.each([
["alert-success", "success"],
["alert-error", "error"],
])("should have className $p when %s", (expectedClassname, type) => {
//Arrange
//Act
render(<Alert message="test" type={type} />);
//Assert
expect(screen.getByRole("alert")).toHaveAttribute("class", expectedClassname);
});
Lets try a more complex example for a component that changes state and interacts with children components
export const Form = ({ headline }) => {
const [showAlert, setShowAlert] = useState(false);
const [alertType, setAlertType] = useState(false);
const [alertMessage, setAlertMessage] = useState("");
const inputEl = useRef(null);
const onSubmit = () => {
api
.postForm(inputEl.current.value)
.then(() => {
setShowAlert(true);
setAlertType("success");
setAlertMessage("Post Success");
})
.catch(() => {
setShowAlert(true);
setAlertType("fail");
setAlertMessage("Failed to post. Please try again");
});
};
return (
<div>
{headline && <h2>{headline}</h2>}
{showAlert && <Alert type={alertType} message={alertMessage} />}
<input type="text" name="username" ref={inputEl} />
<button type="submit" onClick={() => onSubmit()}>
Submit
</button>
</div>
);
};
- Since we don't want to test the implementation details of the child component, we should first mock the
Alert
component
const mockAlert = jest.fn();
jest.mock("../Alert/Alert", () => {
return {
Alert: jest.fn((props) => {
mockAlert(props);
return <div>Alert Mock</div>;
}),
};
});
Mocking a component is very similar to mocking a module, each time the Alert component is used, this <div>Alert Mock</div>
content will be rendered instead.
As for the mockAlert
function, we are using it so we can listen/spy on props that are passed to the Alert
component.
Note: jest only allows this naming convension mockAlert
(prefexed with mock) to be referenced inside jest.mock
2. We can make sure that the Alert is not invoked with component is rendered since showAlert is always false in the beginning
it("should not render Alert when rendered ", () => {
//Arrange
//Act
render(<Form />);
//Assert
expect(mockAlert).not.toBeCalled();
});
- We will also mock the API
postForm
using any of the three methods we defined previously. - Now lets try to fire the click event and test the Alert behavior for success and failure
it("should set type=success and message=Post Success to Alert ", async () => {
//Arrange
render(<Form headline="test" />);
//Act
screen
.getByRole("button", {
name: /Submit/i,
})
.click();
//Assert
await waitFor(() => {
expect(mockAlert).toHaveBeenCalledWith({
type: "success",
message: "Post Success",
});
});
});
it("should set type=fail and message='Failed to post. Please try again' to Alert ", async () => {
//Arrange
jest.spyOn(api, "postForm").mockRejectedValueOnce({});
render(<Form headline="test" />);
//Act
screen
.getByRole("button", {
name: /Submit/i,
})
.click();
//Assert
await waitFor(() => {
expect(mockAlert).toHaveBeenCalledWith({
type: "fail",
message: "Failed to post. Please try again",
});
});
});
Writing tests for async code can be very challenging, RTL has very helpful utilities.
findBy can be useful when we want to assert an element exists, or we want to interact with an element, but that element hasn't appeared into view yet.
It will appear after some async code is executed.
For example: we want to test that the alert appears after api response is successful:
it("should render alert when api response is success", async () => {
//Arrange
jest.spyOn(api, "postForm").mockResolvedValue({});
render(<Form headline="test" />);
//Act
screen
.getByRole("button", {
name: /Submit/i,
})
.click();
//Assert
await screen.findByText("Alert Mock");
});
we can use waitFor when we want to test some logic some async logic. It will wait for a period of time until the expect
inside it succeeds
we aleady used waitFor in the previous example because the Alert will only appear after the promise rejects.
it("should set type=fail and message='Failed to post. Please try again' to Alert ", async () => {
//Arrange
jest.spyOn(api, "postForm").mockRejectedValueOnce({});
render(<Form headline="test" />);
//Act
screen
.getByRole("button", {
name: /Submit/i,
})
.click();
//Assert
+ await waitFor(() => {
expect(mockAlert).toHaveBeenCalledWith({
type: "fail",
message: "Failed to post. Please try again",
});
});
});
We talked before about failing tests to make sure they are not flaky. But if we try to fail the previous test by adding a .not
you will see that the test still succeeds
await waitFor(() => {
+ expect(mockAlert).not.toHaveBeenCalledWith({
type: "fail",
message: "Failed to post. Please try again",
});
});
This happened because waitFor will wait until the expect
succeeds.
And in the first render of this component, mockAlert
wasn't called, so the test succeeds.
waitFor will not wait for all promises to resolve. i.e: it will not wait for the api response to resolve in this case.
So in order for to fail our test we should change the expected args type
and message
await waitFor(() => {
expect(mockAlert).toHaveBeenCalledWith({
+ type: "wrong type",
+ message: "wrong message",
});
});
this will surely fail, because mockAlert
is never called with these args
debugging in vscode is very simple
- add launch.json file from debug tab
- add the following content
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Jest single run all tests",
"program": "${workspaceRoot}/node_modules/jest/bin/jest.js",
"args": ["--verbose", "-i", "--no-cache"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
},
{
"type": "node",
"request": "launch",
"name": "Jest watch all tests",
"program": "${workspaceRoot}/node_modules/jest/bin/jest.js",
"args": ["--verbose", "-i", "--no-cache", "--watchAll"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
},
{
"type": "node",
"request": "launch",
"name": "Jest watch current file",
"program": "${workspaceFolder}/node_modules/jest/bin/jest",
"args": [
"${fileBasename}",
"--verbose",
"-i",
"--no-cache",
"--watchAll"
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
}
]
}
- add your breakpoints and choose any of the defined options from debug tab
- start debugging
Jest Preview is a good package that allows us to visually debug the current dom tree under test.
It is useful when test is failing and you can't see why an element isn't found by jest for example.
for usage you can follow the documentation
https://xp123.com/articles/3a-arrange-act-assert/
https://testing-library.com/docs/queries/about/
https://testing-library.com/docs/react-testing-library/cheatsheet
https://testing-library.com/docs/react-testing-library/api#render
https://testing-library.com/docs/dom-testing-library/api-async
https://testing-library.com/docs/example-react-formik/
https://github.com/bmvantunes/youtube-react-testing-video8-forms-react-testing-library
https://davidwcai.medium.com/react-testing-library-and-the-not-wrapped-in-act-errors-491a5629193b
https://kentcdodds.com/blog/fix-the-not-wrapped-in-act-warning
https://www.querythreads.com/how-to-solve-the-update-was-not-wrapped-in-act-warning-in-testing-library-react/
https://robertmarshall.dev/blog/react-component-props-passed-to-child-jest-unit-test/
https://robertmarshall.dev/blog/how-to-mock-a-react-component-in-jest/
https://create-react-app.dev/docs/running-tests/#filename-conventions
https://medium.com/@jeff_long/organizing-tests-in-jest-17fc431ff850
https://askcodes.net/coding/jest-folder-structure
https://medium.com/@stefanovskyi/unit-test-naming-conventions-dd9208eadbea
https://daily.dev/blog/unit-testing-fraud-why-code-coverage-is-a-lie