Senior Developer Answers The Most Asked Questions About React Testing
This article answers the most Googled questions about React testing, like "What is a unit test in React?", "How do you test code with REST API calls?", "When should you use act
with React Testing Library?", and so much more.
To kick things off, we're going to start with:
What is unit testing in React?
When people talk about "unit testing in React," they typically mean tests for some helper functions, reducers, hooks, or React components.
We could move on to the next question, but here's the catch:
You may not know there are different kinds of tests.
1.) Unit Tests
Unit tests test something in isolation.
I recommend three test frameworks to you: Jest and Vitest because they're the most popular, and RITEway because it has an API that forces you to write good tests.
For example, you can have a test for a pure function add
that takes two numbers and returns their sum.
import { describe, expect, test } from 'vitest';
const add = (a: number, b: number) => a + b;
describe('add()', () => {
test('given two numbers: returns the sum', () => {
const actual = add(1, 2);
const expected = 3;
expect(actual).toEqual(expected);
});
});
If you've never seen a test before, they usually look similar to this. There is a describe
function that tells you about the component you're testing.
Then there is a test
function that contains prose describing what behavior you're testing.
Finally, there is an expect
function responsible for asserting that the behavior you're testing is what you expect.
Unit tests are ideal for pure functions that, given the same input, always return the same output and lack any side effects.
Usually, functions with side effects are better tested with ...
2.) Integration Tests
Integration tests test how different things work together.
You can use the same test frameworks for integration tests as you do for unit tests.
Assume you have a function requireUserExists
that takes a user ID and returns a user if it exists; otherwise, it throws a 404 error.
import { UserAccount } from '@prisma/client';
import { throwIfEntityIsMissing } from '~/utils/throw-if-entity-is-missing.server';
export async function requireUserExists(userId: UserAccount['id']) {
const user = await retrieveUserAccountFromDatabaseById(userId);
return throwIfEntityIsMissing(user);
}
This function requires an integration test because both requireUserExists
consists of two functions and it has side effects.
requireUserExists
usesretrieveUserAccountFromDatabaseById
andthrowIfEntityIsMissing
. Therefore, any test forrequireUserExists
will also test whether these two functions successfully work together to create the desired outcome.- Additionally,
requireUserExists
has two side effects. As a reminder, any observable interaction with the outside world or the program's state outside the function's scope is a side effect. First,requireUserExists
retrieves a user from the database, which is an asynchronous operation. Second, it throws an error if the user is not found.
Here's how you can write tests for requireUserExists
.
import { describe, expect, onTestFinished, test } from 'vitest';
import { createPopulatedUserAccount } from '~/features/user-account/user-account-factories.server';
import { requireUserExists } from '~/features/user-account/user-account-helpers.server';
import {
deleteUserAccountFromDatabaseById,
saveUserAccountToDatabase,
} from '~/features/user-account/user-account-model.server';
async function setup() {
const user = createPopulatedUserAccount();
await saveUserAccountToDatabase(user);
onTestFinished(async () => {
await deleteUserAccountFromDatabaseById(user.id);
});
return { user };
}
describe('requireUserExists()', () => {
test('given a user ID for an existing user: returns the user', async () => {
const { user } = await setup();
const actual = await requireUserExists(user.id);
const expected = user;
expect(actual).toEqual(expected);
});
test('given a user ID for a non-existing user: throws a 404 error', async () => {
expect.assertions(1);
const userId = 'non-existing-user-id';
try {
await requireUserExists(userId);
} catch (error) {
if (error instanceof Response) {
expect(error.status).toEqual(404);
}
}
});
});
When you write integration tests that modify a database, you either need to mock the database or write setup and teardown code to clean up after the test. If you're unfamiliar with mocking, it means replacing a function with a test double.
You should know that "Mocking Is A Code Smell", but that's a topic for another article. In this tutorial, you're going to use a setup function.
It uses a factory function createPopulatedUserAccount
to create a user and then saves it to the database.
It then uses the onTestFinished
hook, which runs after the test has finished and deletes the user from the database.
Using that function, you can write two tests.
The first one tests that the function returns the user if it exists. The setup function is called within the test, saves a user to the database, and automatically deletes it after the test has finished.
The second one tests that the function throws an error if the user does not exist.
For this test, you need to call expect.assertions(1)
to tell Vitest that you want requireUserExists
to fail and one assertion to occur. You're using try
and catch
here, and if requireUserExists
succeeds and doesn't throw an error, the test will fail because of the expect.assertions(1)
call. If it throws as you expect, the test will go into the catch
block, execute the expect
assertion, and the test will pass.
And finally, there are also ...
3.) Functional Tests
Functional tests test your app from your user's perspective.
You can either write functional tests for React components with the previously mentioned frameworks together with React Testing Library (RTL), or you can use a framework like Playwright or Cypress that spins up an actual browser.
React Testing Library is used together with Jest or Vitest to write functional tests for your React components, and you mock the DOM using JSDOM or Happy DOM. You will learn how to install those and use React Testing Library later in this article.
An E2E test framework like Playwright or Cypress uses an actual browser to visit your app and then interact with it.
RTL runs faster than E2E test frameworks, but E2E test frameworks give you more confidence that your app works as expected.
Here's what a functional test using Playwright looks like.
import { expect, test } from '@playwright/test';
test.describe('example.com', () => {
test("given any user: can visit the page and read its text", async ({
page,
}) => {
// Navigate to the page.
await page.goto('https://example.com');
// Verify the heading is present.
await expect(
page.getByRole('heading', { name: 'Example Domain', level: 1 }),
).toBeVisible();
// Verify the page contains expected text.
await expect(
page.getByText('This domain is for use in illustrative examples'),
).toBeVisible();
// Verify the link to more information.
await expect(
page.getByRole('link', { name: 'More information...' }),
).toHaveAttribute('href', 'https://www.iana.org/domains/example');
});
});
You visit a page and then verify that the page contains the expected headings, texts, and links. You can also interact with the page, like clicking buttons or filling out forms, but more on that later.
These definitions for the different types of tests are not always clear-cut. The line between unit tests, integration tests, and functional tests is blurry.
For example, notice how tests for a reducer usually test its actions, its selectors, and the reducer together, so they could also be called integration tests.
describe('selectMondayIsActive() selector', () => {
test('given initial state: returns true', () => {
const state = rootReducer();
const actual = selectMondayIsActive(state);
const expected = true;
expect(actual).toEqual(expected);
});
test('given Monday is deactivated: returns false', () => {
const state = rootReducer(undefined, toggleMondayIsActive());
const actual = selectMondayIsActive(state);
const expected = false;
expect(actual).toEqual(expected);
});
test('given Monday is activated again: returns true', () => {
const actions = [toggleMondayIsActive(), toggleMondayIsActive()];
const state = actions.reduce(rootReducer, rootReducer());
const actual = selectMondayIsActive(state);
const expected = true;
expect(actual).toEqual(expected);
});
});
And tests for React components usually interact with the component from the perspective of a user, so they could be called functional tests. But, if the React component renders another component or uses a hook, it could also be called an integration test because it consists of multiple units. Still, if you only test that component in isolation, it could also be called a unit test.
You will develop a feeling for what to call a test based on the context over time.
Remember:
“When in Rome, do as the Romans do.” - Julius Caesar
In other words, if you're working in a team, you should follow the conventions of your team.
Now let's move on to the next question:
How to do unit testing in React?
In your React project, install Vitest.
npm install --save-dev vitest
If you're reading this article in the future, for the latest instructions on how to set up Vitest, check out their docs.
Add a test script to your package.json
.
{
"scripts": {
"test": "vitest --reporter=verbose"
}
}
Create a quick sanity check test to make sure your configuration worked.
import { describe, expect, test } from "@jest/globals";
describe("sanity check", () => {
test("true is true", () => {
expect(true).toEqual(true);
});
});
It simply asserts that true
is true
.
Run your tests:
npm test
You should see the test pass.
DEV v2.1.5 /Users/jan/dev/senior-developer-answers-the-most-asked-questions-about-react-testing
✓ app/sanity-check.test.ts (1)
✓ sanity check (1)
✓ true is true
Test Files 1 passed (1)
Tests 1 passed (1)
Start at 23:27:28
Duration 333ms (transform 19ms, setup 59ms, collect 4ms, tests 1ms, environment 108ms, prepare 33ms)
PASS Waiting for file changes...
press h to show help, press q to quit
Now add React Testing Library.
npm install --save-dev @testing-library/react @testing-library/dom @testing-library/jest-dom @types/react @types/react-dom happy-dom @vitejs/plugin-react vite-tsconfig-paths
Create a vitest.config.mts
file in the root of your project:
import react from '@vitejs/plugin-react';
import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [react(), tsconfigPaths()],
server: {
port: 3000,
},
test: {
environment: 'happy-dom',
globals: true,
setupFiles: ['./src/tests/setup-test-environment.ts'],
include: ['./src/**/*.{spec,test}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
exclude: [
'**/node_modules/**',
'**/build/**',
'**/postgres-data/**',
],
coverage: {
reporter: ['text', 'json', 'html'],
},
},
});
Then create a file called src/tests/setup-test-environment.ts
to set up your test environment.
import '@testing-library/jest-dom/vitest';
// See https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html#configuring-your-testing-environment.
// @ts-expect-error globalThis is not defined in TypeScript.
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
Now you are ready to write your React component tests with React Testing Library.
What does testing a React app with Jest do?
Jest is a test runner. It provides helper functions so you can write tests and executes those functions for you when you run your tests. This tutorial uses Vitest because it has the same API as Jest but runs faster. Everything you learn in this article is applicable to Jest.
The main difference in usage is that Jest is frequently configured to expose its methods globally. This means that in any file with .test.ts
or .test.tsx
, you can use describe
, test
, expect
, and so on without importing them.
Vitest or Jest alone is a bad choice for writing functional tests for your React apps, though.
You should use React Testing Library, too. With RTL, you can simulate user interactions.
React Testing Library, together with Happy DOM or Jest DOM, renders your React component in a mocked browser environment for you.
Testing your React app with RTL and Jest or Vitest gives you confidence that your app works as expected.
How to run React Testing Library?
If you've set up React Testing Library with Vitest as described above, you can now run your React component tests.
import { render, screen } from '@testing-library/react';
import { describe, expect, test } from 'vitest';
function Greeting() {
return (
<div>
<h1>Hello World</h1>
<p>Some description</p>
</div>
);
}
describe('Greeting component', () => {
test('renders a greeting', () => {
render(<Greeting />);
expect(
screen.getByRole('heading', { name: /hello world/i }),
).toBeInTheDocument();
expect(screen.getByText(/some description/i)).toBeInTheDocument();
});
});
This example shows you how to test a function component. You use React Testing Library's render
function to render the component, and then you use the screen
object to query the DOM.
The same works for class components because Testing Library abstracts away the implementation details for you.
import { render, screen } from '@testing-library/react';
import { Component } from 'react';
import { describe, expect, test } from 'vitest';
class StayFocused extends Component {
render() {
return <h2>Stay focused!</h2>;
}
}
describe('StayFocused component', () => {
test('renders a stay focused call to action', () => {
render(<StayFocused />);
expect(
screen.getByRole('heading', { name: /stay focused/i }),
).toBeInTheDocument();
});
});
So everything you learn about React testing going forward applies to class components as well.
I want to point out the "blurriness" of test definitions here one more time. Notice how both of these tests test the component from the perspective of a user. Therefore, you can legitimately call them functional tests. But they also test the component in isolation, so they could also be called unit tests.
How to test a function in React Testing Library?
If you mean a function component, scroll back up to the part where you test the first React component, Greeting
, because that's a function component.
If you're talking about functions that are being passed as props to your React components, you need to mock them. This allows you to test that your functions are called as expected.
How to mock a function in React Testing Library?
You can create a mock function with vi.fn()
. In Jest, you use jest.fn()
.
In other words, the ability to mock has nothing to do with React Testing Library. Your respective test runner or framework provides the mock functions.
const onChange = vi.fn();
render(<SomeComponent onChange={onChange} />);
Every time you call a mock function, it stores its call arguments, returns, and instances. You can then make assertions on the mock function.
Let's use mocking and look at an example of a button to show you:
How to test a button with React Testing Library?
Create a simple button component and write a test for it.
import { render, screen } from '@testing-library/react';
import type { MouseEventHandler } from 'react';
import { describe, expect, test, vi } from 'vitest';
function Button({
onClick,
}: {
onClick: MouseEventHandler<HTMLButtonElement>;
}) {
return <button onClick={onClick}>Click me</button>;
}
describe('Button', () => {
test('given an onClick function and the button is clicked: calls the onClick function', () => {
const onClick = vi.fn();
render(<Button onClick={onClick} />);
const button = screen.getByRole('button', { name: /click me/i });
button.click();
expect(onClick).toHaveBeenCalled();
});
});
You mock the onClick
function and then fire a click event directly on the button element via button.click()
. This will call the mock, and you can assert that it was called.
Testing Library also exposes the fireEvent
function, which you can use to fire events on the component.
import { fireEvent, render, screen } from '@testing-library/react';
import type { MouseEventHandler } from 'react';
import { describe, expect, test, vi } from 'vitest';
function Button({
onClick,
}: {
onClick: MouseEventHandler<HTMLButtonElement>;
}) {
return <button onClick={onClick}>Click me</button>;
}
describe('Button', () => {
test('given an onClick function and the button is clicked: calls the onClick function', () => {
const onClick = vi.fn();
render(<Button onClick={onClick} />);
const button = screen.getByRole('button', { name: /click me/i });
fireEvent.click(button);
expect(onClick).toHaveBeenCalled();
});
});
The difference between button.click()
and fireEvent.click()
lies in how they trigger the click event. button.click()
directly calls the DOM element's click method, bypassing React's synthetic event system and only firing the click event. In contrast, fireEvent.click()
simulates the click event using dispatchEvent
, integrating with React's event system but still only triggering the click event without intermediate events like mousedown
or focus
.
For more realistic user interactions, you want to use the userEvent
library because it simulates full user interactions. So install it.
npm install --save-dev @testing-library/user-event
Next, import it in your test and use the setup
method to create a user.
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import type { MouseEventHandler } from 'react';
import { describe, expect, test, vi } from 'vitest';
function Button({
onClick,
}: {
onClick: MouseEventHandler<HTMLButtonElement>;
}) {
return <button onClick={onClick}>Click me</button>;
}
describe('Button', () => {
test('given an onClick prop and the button is clicked: calls the onClick function', async () => {
const user = userEvent.setup();
const onClick = vi.fn();
render(<Button onClick={onClick} />);
await user.click(screen.getByRole('button', { name: /click me/i }));
expect(onClick).toHaveBeenCalled();
});
});
You can then let the user click the button and assert that the mock was called. This mimics user actions by triggering the same browser events those actions would cause. The user
object exposes many other interactions, like typing, selecting options, and so on.
If the interaction with the button changes some internal state that you can observe in your user interface, then you do NOT need to mock anything.
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useState } from 'react';
import { describe, expect, test } from 'vitest';
function Button() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>Click me</button>
<div>{count}</div>
</div>
);
}
describe('Button', () => {
test('given the button is clicked: increments the count', async () => {
render(<Button />);
expect(screen.getByText(/0/i)).toBeInTheDocument();
expect(screen.queryByText(/1/i)).not.toBeInTheDocument();
await user.click(screen.getByRole('button', { name: /click me/i }));
expect(screen.getByText(/1/i)).toBeInTheDocument();
});
});
In this test, your component renders the current count, which you can increment by clicking the button.
Then you assert that, by default, the count is 0 and that the incremented count is NOT shown. To test for the absence of elements, you need to use the queryBy
instead of the getBy
query.
Finally, you use Testing Library's userEvent
to click the button and assert that the count was incremented.
Of all four button tests that you saw, this one is the cleanest because it only interacts with your React component as your users would, without the need to mock, making it the most "functional" test. If you need to mock, you should still prefer using userEvent
over fireEvent
or the native DOM methods.
As a summary:
- Calling methods like
button.click()
calls the DOM element's click method, bypassing React's synthetic event system and only firing the click event. - Calling
fireEvent.click()
simulates the click event usingdispatchEvent
, integrating with React's event system but still only triggering the click event without intermediate events likemousedown
orfocus
. - Using
userEvent
simulates full user interactions, including intermediate events likemousedown
orfocus
.
How to debug React Testing Library?
If you want to debug your tests, call the screen.debug()
method.
import { render, screen } from '@testing-library/react';
import { describe, test } from 'vitest';
describe('screen.debug example', () => {
test('demonstrates screen.debug usage', () => {
render(<div>Hello World</div>);
// Log the current state of the DOM.
screen.debug();
// Log a specific element.
screen.debug(screen.getByText('Hello World'));
});
});
There are two ways to use screen.debug()
. Calling it without arguments will print out the whole DOM tree to the console.
<body>
<div>
<div>
Hello World
</div>
</div>
</body>
You can see here that render
implicitly wraps the component in a body
and a div
.
If you want to debug a specific element and its children, you can pass a selector to screen.debug()
.
screen.debug(screen.getByText('Hello World'));
This will print out the DOM tree around the element.
<div>
Hello World
</div>
To debug the button test you saw before, you can also add console.log
s wherever you want.
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import type { MouseEventHandler } from 'react';
import { describe, expect, test, vi } from 'vitest';
function Button({
onClick,
}: {
onClick: MouseEventHandler<HTMLButtonElement>;
}) {
return (
<button
onClick={event => {
onClick(event);
console.log('after the button click');
}}
>
Click me
</button>
);
}
describe('Button', () => {
test('given an onClick prop and the button is clicked: calls the onClick function', async () => {
const user = userEvent.setup();
const onClick = vi.fn();
render(<Button onClick={onClick} />);
await user.click(screen.getByRole('button', { name: /click me/i }));
expect(onClick).toHaveBeenCalled();
});
});
This will then show up in the output of your test.
stdout | app/example.test.tsx > Button > given an onClick prop and the button is clicked: calls the onClick function
after the button click
✓ app/example.test.tsx (1)
✓ Button (1)
✓ given an onClick prop and the button is clicked: calls the onClick function
Test Files 1 passed (1)
Tests 1 passed (1)
Start at 10:56:02
Duration 153ms
PASS Waiting for file changes...
press h to show help, press q to quit
How to mock an API in React Testing Library?
The best way is to use MSW, which stands for Mock Service Worker.
Install MSW in your project.
npm install msw --save-dev
Now you can write a test for a component called Fetcher
that fetches a user from a placeholder API.
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { http } from 'msw';
import { setupServer } from 'msw/node';
import { useState } from 'react';
import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest';
function Fetcher() {
const [user, setUser] = useState<{ name: string; id: string } | null>(null);
return (
<div>
<button
onClick={() => {
fetch('https://jsonplaceholder.typicode.com/users/1')
.then(response => response.json())
.then(setUser);
}}
>
Fetch
</button>
<div>{user ? `User name: ${user.name}` : 'Please fetch the user.'}</div>
</div>
);
}
const server = setupServer(
http.get(
'https://jsonplaceholder.typicode.com/users/1',
async () =>
new Response(JSON.stringify({ name: 'Jane Doe', id: '1' }), {
headers: { 'Content-Type': 'application/json' },
}),
),
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
describe('Fetcher', () => {
test("given the fetch button is clicked: renders the user's name", async () => {
const user = userEvent.setup();
render(<Fetcher />);
await user.click(screen.getByRole('button', { name: /fetch/i }));
expect(await screen.findByText(/user name: jane doe/i)).toBeInTheDocument();
});
});
The Fetcher
component renders a button. When clicked, it fetches a user from a placeholder API.
You use setupServer
to create a mock server. The server intercepts the fetch request and returns a response you define. Intercepting requests gives you faster tests than making actual API requests and makes the response consistent.
To integrate MSW with a test runner, use the beforeAll
, afterEach
, and afterAll
hooks to set up the following steps:
- Start mocking before tests begin using
server.listen()
. - Reset request handlers between tests with
server.resetHandlers()
. - Restore native request modules after tests finish using
server.close()
.
How to test a dropdown in React?
Create a dropdown component so you can write tests for it.
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, expect, test } from 'vitest';
function Dropdown() {
return (
<div>
<label htmlFor="fruits">Pick a fruit:</label>
<select name="fruits" id="fruits">
<option value="banana">Banana</option>
<option value="orange">Orange</option>
<option value="apple">Apple</option>
</select>
</div>
);
}
describe('Dropdown', () => {
test('given the user picks values: renders the correct value', async () => {
const user = userEvent.setup();
render(<Dropdown />);
expect(screen.getByDisplayValue(/banana/i)).toBeInTheDocument();
expect(screen.queryByDisplayValue(/orange/i)).not.toBeInTheDocument();
const select = screen.getByLabelText(/pick a fruit/i);
await user.selectOptions(select, 'orange');
expect(screen.getByDisplayValue(/orange/i)).toBeInTheDocument();
});
});
The Dropdown
component renders a label and a select
element. In this tutorial, you're going to create it using a select
element, but it will work the same way with a custom component from some component library.
Now you can use the selectOptions
method from userEvent
to select an option from the dropdown and assert that the correct value is rendered.
How to test useEffect in React Testing Library?
Did you know that React 18 and up runs useEffect
twice on mount in strict mode? Let me know on X if you want me to write an article to break down how many React developers are using useEffect
wrong.
Anyway, let's look at a correct usage of useEffect
in an OfflineIndicator
component.
import { act, render, screen } from '@testing-library/react';
import { useEffect, useState } from 'react';
import { describe, expect, test } from 'vitest';
function OfflineIndicator() {
const [isOffline, setIsOffline] = useState(false);
useEffect(() => {
if (typeof window !== 'undefined') {
function handleOnline() {
setIsOffline(false);
}
function handleOffline() {
setIsOffline(true);
}
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return function cleanup() {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}
}, []);
return <div>{isOffline ? 'Offline' : 'Online'}</div>;
}
describe('OfflineIndicator', () => {
test('given the user switches between offline and online: renders the correct state', () => {
render(<OfflineIndicator />);
expect(screen.getByText(/online/i)).toBeInTheDocument();
act(() => {
window.dispatchEvent(new Event('offline'));
});
expect(screen.getByText(/offline/i)).toBeInTheDocument();
act(() => {
window.dispatchEvent(new Event('online'));
});
expect(screen.getByText(/online/i)).toBeInTheDocument();
});
});
The OfflineIndicator
component uses useEffect
to detect when the user goes offline or online. It then renders a div
element that shows a corresponding string about the state.
You can write a test for the OfflineIndicator
component that checks if the component renders the correct state when the user goes offline or online. To do this, you need to simulate the user going offline and online, which you do with window.dispatchEvent
. You wrap this in act
to ensure all updates have been processed and applied to the DOM before you make any assertions. You're going to learn more about act
in a moment. But before that, I want to point out something important.
When you test a useEffect
hook inside a component, you only test what the user sees as a result of the useEffect
hook. This is what Kent C. Dodds, the creator of React Testing Library, means when he says:
“Avoid testing implementation details.” - Kent C. Dodds
Avoiding testing implementation details means focusing on what the user sees instead of how the component works under the hood. Test behaviors and visible outputs, like rendered elements or text, instead of functions, hooks, or internal states. This keeps tests resilient to refactoring because they break only when functionality or user experience changes—NOT when internal code changes.
act
?
React Testing Library: When to use In the example above, you used act
when simulating switching between online and offline.
React handles updates asynchronously. When you change the state of a component, React schedules a re-render of the component and then decides at some point to apply the changes to the DOM.
act()
ensures all updates have been processed and applied to the DOM before you make any assertions.
Testing Library wraps its handlers—such as fireEvent
or userEvent
—in act
for you. But since dispatching the online and offline events is custom, you need to wrap them in act
yourself.
How to test hooks in React?
You can test hooks without using them inside a component.
Extract the offline detection logic into a custom hook.
import { act, renderHook } from '@testing-library/react';
import { useEffect, useState } from 'react';
import { describe, expect, test } from 'vitest';
export function useIsOffline() {
const [isOffline, setIsOffline] = useState(false);
useEffect(() => {
if (typeof window !== 'undefined') {
function handleOnline() {
setIsOffline(false);
}
function handleOffline() {
setIsOffline(true);
}
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return function cleanup() {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}
}, []);
return isOffline;
}
describe('useIsOffline()', () => {
test('detects whether the user is offline or not', () => {
const { result } = renderHook(() => useIsOffline());
expect(result.current).toEqual(false);
act(() => {
window.dispatchEvent(new Event('offline'));
});
expect(result.current).toEqual(true);
act(() => {
window.dispatchEvent(new Event('online'));
});
expect(result.current).toEqual(false);
});
});
The custom useIsOffline
hook is straightforward. You simply use the same useEffect
hook to set up the event listeners and return the isOffline
state.
To test custom hooks, you can use the renderHook
function from @testing-library/react
. renderHook
exposes a .current
property on its result
object to provide access to the latest value returned by the hook being tested. You can think of result
as a ref
for the most recently committed value.
Apart from that, the test does exactly the same thing as the test for the OfflineIndicator
component.
How to do E2E testing in React?
End-to-end (E2E) testing is the practice of testing the entire application from start to finish. That means running the application in a real browser, interacting with it as a user would, and making requests to a real database.
E2E testing is a subset of functional tests. The more realistic your functional tests are, the more you can call them E2E tests.
By definition, E2E tests do NOT care about what framework you're using. So any test written with E2E test frameworks like Playwright, Cypress, or Selenium is how you do E2E testing with React.
Here's another example E2E test for you. Again, this test will work with React as well as with raw HTML and Vanilla JavaScript.
import { expect, test } from '@playwright/test';
test.describe('Wikipedia Search', () => {
test('page should let you search for "Playwright" and render an article about it', async ({
page,
}) => {
// Navigate to Wikipedia's homepage.
await page.goto('https://www.wikipedia.org/');
// Select the search input field using its role and accessible name.
const searchInput = page.getByRole('searchbox', {
name: 'Search Wikipedia',
});
await searchInput.fill('Playwright');
// Click the search button using its role and accessible name.
const searchButton = page.getByRole('button', { name: 'Search' });
await searchButton.click();
// Verify that the page title contains "Playwright".
await expect(page).toHaveTitle(/Playwright/);
// Optionally, verify that the first heading matches "Playwright".
const firstHeading = page.getByRole('heading', {
level: 1,
name: 'Playwright',
});
await expect(firstHeading).toHaveText('Playwright');
});
});
You can write a test that spins up a real browser, visits the Wikipedia homepage, searches for "Playwright," and verifies that the page title contains "Playwright."
It is an E2E test because it uses a real browser, makes real network requests, and queries Wikipedia's database.