đ§Ș 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
Quick Links
Legal Stuff
Social Media