🧪 Mocking Browser APIs and Modules in React Tests
In the world of frontend testing, one of the most common challenges developers face is:
“How do I test code that relies on Browser APIs or external modules?”
Let’s be honest—our test environments aren’t real browsers. But thanks to tools like Jest and jsdom, we can simulate them. And when simulation isn’t enough, we can mock our way through it. Let’s learn how to do that effectively without sacrificing too much confidence in our tests.
When you mock something, you’re replacing the real thing (API, module, etc.) with a fake version for the sake of testing.
You’re intentionally “poking a hole in reality” — which reduces confidence but improves speed and control.
That’s why mocking is best reserved for:
geolocation
, canvas
, or media queries)📖 Learn more: 👉 The Merits of Mocking 👉 What is a JavaScript mock?
Here’s the truth:
⚠️ Jest tests run in Node.js, not a real browser.
Instead, they use a browser-like simulation library called jsdom. jsdom does a great job mimicking the DOM—but it’s not perfect.
For example:
window.navigator.geolocation.getCurrentPosition()
is not implemented in jsdomwindow.resizeTo()
doesn’t existmatchMedia
is unimplementedSo what do we do?
✅ We mock them.
Here’s an example from testing a useMedia
hook that relies on matchMedia
and responds to window resizing.
import matchMediaPolyfill from 'mq-polyfill'beforeAll(() => {matchMediaPolyfill(window)window.resizeTo = function resizeTo(width, height) {Object.assign(this, {innerWidth: width,innerHeight: height,outerWidth: width,outerHeight: height,}).dispatchEvent(new this.Event('resize'))}})
This lets us simulate media queries and resizing—even in Node.
Let’s say you have a Location
component that calls:
navigator.geolocation.getCurrentPosition(success, error)
Since getCurrentPosition
doesn’t exist in jsdom, we need to mock it in our test:
beforeEach(() => {const fakePosition = {coords: {latitude: 51.1,longitude: 45.3,},}window.navigator.geolocation = {getCurrentPosition: jest.fn((success) => {success(fakePosition)}),}})
This approach:
getCurrentPosition
with a mockact()
React testing sometimes requires wrapping updates in act()
to make sure state updates are processed before assertions.
import { act } from 'react-dom/test-utils'await act(async () => {// run the mocked getCurrentPosition})
Usually, libraries like React Testing Library wrap act()
for you. But when mocking asynchronous behavior like geolocation
, you may need it manually.
📚 Learn more: Fix the “not wrapped in act(…)” warning
Sometimes, you’re not mocking a browser API—you’re mocking a module your code imports.
For example, here’s a simple math module:
// math.jsexport const add = (a, b) => a + bexport const subtract = (a, b) => a - b
And in the test:
import { add, subtract } from '../math'jest.mock('../math')// `add` and `subtract` are now Jest mock functionsadd.mockImplementation(() => 42)expect(add).toHaveBeenCalledWith(2, 3)
If you only want to mock part of a module:
jest.mock('../math', () => {const actual = jest.requireActual('../math')return {...actual,subtract: jest.fn(),}})
This keeps add()
as-is, but mocks subtract()
.
📖 More on this: Manual Mocks – Jest Docs
If you’re using a custom hook that relies on a browser API (like useGeoLocation
), you might choose to mock the hook instead of the API.
jest.mock('../hooks/useGeoLocation', () => () => ({latitude: 12.34,longitude: 56.78,}))
This is especially useful when:
💡 Bonus tip: You can mock a hook with another hook—just return mock state using useState()
or useEffect()
inside.
Don’t just test success cases. Always test what happens when errors occur.
window.navigator.geolocation.getCurrentPosition = jest.fn((_, errorCallback) => {errorCallback({ message: 'Permission denied' })})expect(screen.getByRole('alert')).toHaveTextContent(/permission denied/i)
Make sure your components fail gracefully, show error messages, and remain accessible.
Mocking Target | When to Use |
---|---|
window APIs | When jsdom doesn’t support them (geolocation , matchMedia , etc.) |
Custom hooks | When mocking APIs is too complex or unnecessary |
External modules | When a module does something side-effecty (like making requests) |
Use act() manually | When mocking async behavior that affects state |
Mocking is a double-edged sword: powerful, but easy to misuse. Keep these principles in mind:
And always remember: the more realistic your tests, the more confidence they give you.
Fill out the elaboration and feedback form to lock in what you’ve learned:
👉 Mocking Browser APIs – Feedback Form