I spent last Thursday afternoon staring at a terminal window that was spitting out the same cryptic error message for three hours straight. You know the one. It wasn’t a logic error. My component worked fine in the browser. It was the test environment choking on a third-party charting library that apparently needs a full WebGL context just to exist.
Testing in 2026 is supposed to be better than this. We have better tools, faster runners, and AI assistants that write half the boilerplate for us. Yet here we are, still fighting with Jest mocks like it’s 2019.
If you’re using React Testing Library (RTL), you’ve probably heard the mantra: “Test your components the way users use them.” It’s good advice. Users don’t care about implementation details. But users also don’t run your app inside a Node.js process that pretends to be a browser (looking at you, JSDOM). Sometimes, you have to fake things just to get the test to run.
So, let’s talk about mocking. Not the basic “replace a function with jest.fn()” stuff. I mean the messy, complex scenarios that actually break your CI pipeline.
The “Heavy Component” Problem
Here is the scenario that broke my spirit last week. I had a dashboard widget. It fetches some data, does some math, and renders a heavy chart. Simple enough.
The problem? The charting library tries to access window.ResizeObserver and some canvas APIs immediately upon import. JSDOM throws a fit. The test fails before it even renders anything.
My first instinct was to polyfill everything. I started adding mock implementations for ResizeObserver. Then the canvas context. Then something else. It was a game of whack-a-mole.
Bad approach. If a child component is purely presentational and relies on heavy browser APIs, don’t test it in a unit test. Mock it out completely.
Here is how I handle this now without losing my mind. Instead of mocking the browser APIs, I mock the module itself using Jest’s factory parameter.
// DashboardWidget.test.js
import { render, screen, waitFor } from '@testing-library/react';
import DashboardWidget from './DashboardWidget';
// 1. Mock the heavy library completely
// We don't care if the chart renders pixels correctly in this test.
// We care that the widget passes the correct PROPS to the chart.
jest.mock('../components/HeavyChart', () => {
return function DummyChart(props) {
return (
<div data-testid="mock-chart">
Chart Data Points: {props.data.length}
</div>
);
};
});
test('renders chart with correct data after fetch', async () => {
render(<DashboardWidget />);
// Verify loading state first
expect(screen.getByText(/loading/i)).toBeInTheDocument();
// Wait for data to load and our mock to appear
const chart = await screen.findByTestId('mock-chart');
expect(chart).toHaveTextContent('Chart Data Points: 5');
});
Notice what I did there? I didn’t just return null. I returned a dummy component that renders the props as text. This is a trick I picked up a few years ago and it saves so much time. It lets you assert that the parent passed the right data to the child without actually running the child’s code.
Mocking Modules That Export Named Functions
Another thing that trips people up is partial mocking. Say you have a utility file api.js that exports twenty functions. You only want to mock getUser, but keep the real implementation for formatDate.
I used to mess this up constantly by overwriting the whole module. The syntax for this has always been a bit awkward in Jest, but you have to get it right or your imports come back undefined.
// api.js
export const getUser = () => { /* fetch calls */ };
export const formatDate = (date) => { /* logic */ };
// In your test file
import * as api from './api';
jest.mock('./api', () => {
const originalModule = jest.requireActual('./api');
return {
__esModule: true,
...originalModule,
getUser: jest.fn(),
};
});
test('formats date correctly but mocks user fetch', async () => {
// Setup the return value for this specific test
api.getUser.mockResolvedValue({ name: 'Tech Lead' });
// ... render component ...
});
The jest.requireActual part is the magic sauce. Without it, formatDate disappears, and your component crashes because it’s trying to call a function that doesn’t exist anymore.
The “Act” Warning Nightmare
If you’ve written React tests, you’ve seen the red wall of text: “Warning: An update to Component inside a test was not wrapped in act(…)”.
It’s 2026, and this warning still haunts my dreams. Usually, it happens because of a promise resolving after your test has finished or asserted something. You mocked a network request, but you didn’t wait for the state update to finish before the test exited.
The fix isn’t to sprinkle act() everywhere manually (RTL does that for you mostly). The fix is to make sure your assertions wait for the UI to settle.
I see this pattern a lot in code reviews:
// ❌ DON'T DO THIS
fireEvent.click(button);
expect(api.submitData).toHaveBeenCalled();
// The component might be setting state 'success' here, causing the warning
When you fire an event that triggers an async process, you need to await the outcome in the UI, not just check the mock.
// ✅ DO THIS
userEvent.click(button);
// Wait for the UI change that happens AFTER the promise resolves
await screen.findByText(/success/i);
// NOW check the mock
expect(api.submitData).toHaveBeenCalled();
By waiting for the “Success” message, you implicitly tell Jest to wait for the promise chain to resolve and the React state updates to flush. No more act() warnings.
Why I stopped mocking fetch (mostly)
For a long time, I manually mocked global.fetch or used jest.mock('axios'). It works, but it’s brittle. You end up coupling your tests to the specific HTTP client implementation. If you switch from Axios to Fetch, you have to rewrite all your tests.
I switched to Mock Service Worker (MSW) a couple of years ago and haven’t looked back. Instead of mocking the function that makes the call, you mock the network layer itself.
It’s slightly more setup upfront, but it pays off. Your component makes a real request, MSW intercepts it at the network level, and returns the JSON you defined. This means you can swap out your data fetching library entirely and your tests won’t even notice. That’s the dream, right?
When to Mock vs. When to Stub
Here is my rule of thumb after breaking too many builds:
- Logic hooks: Don’t mock them. Test the component that uses them. If the hook is complex, write a separate test for the hook using
renderHook. - UI Components (Buttons, Inputs): Never mock these. Render them.
- Complex 3rd Party Libs (Charts, Maps, Editors): Mock them immediately. Return a simple div with a
data-testid. You aren’t responsible for testing Highcharts or Google Maps. You’re responsible for testing that you passed them the right lat/long coordinates.
Testing is a trade-off between confidence and speed. Mocking too much gives you speed but zero confidence (you’re essentially testing your mocks). Mocking too little gives you high confidence but slow, flaky tests that fail because a canvas element couldn’t render in a fake browser.
Find the balance. And seriously, if a test takes more than three hours to debug, delete it and write a simpler one. Life is too short.











