Mocking HTTP requests is one thing, but sometimes you have entire Browser APIs or modules that you need to mock. Every time you create a fake version of what your code actually uses, you’re “poking a hole in reality” and you lose some confidence as a result (which is why E2E tests are critical). Remember, we’re doing it and recognizing that we’re trading confidence for some practicality or convenience in our testing. (Read more about this in my blog post: The Merits of Mocking).
To learn more about what “mocking” even is, take a look at my blog post But really, what is a JavaScript mock?
I need to tell you a little secret and I want you to promise me to not be mad…
Our tests aren’t running in the browser 😱😱😱😱😱
It’s true. They’re running in a simulated browser environment in Node. This is
done thanks to a module called jsdom. It does
its best to simulate the browser and implement standards. But there are some
things it’s simply not capable of simulating today. One example is window resize
and media queries. In my
Advanced React Hooks workshop,
I teach something using a custom useMedia
hook and to test it, I have to mock
out the browser window.resizeTo
method and polyfill window.matchMedia
.
Here’s how I go about doing that:
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 allows me to continue to test with Jest (in node) while not actually running in a browser.
So why do we go through all the trouble? Because the tools we currently have for testing are WAY faster and WAY more capable when run in node. Most of the time, you can mock browser APIs for your tests without losing too much confidence. However, if you are testing something that really relies on browser APIs or layout (like drag-and-drop) then you may be better served by writing those tests in a real browser (using a tool like Cypress).
Sometimes, a module is doing something you don’t want to actually do in tests. Jest makes it relatively simple to mock a module:
// math.jsexport const add = (a, b) => a + bexport const subtract = (a, b) => a - b// __tests__/some-test.jsimport {add, subtract} from '../math'jest.mock('../math')// now all the function exports from the "math.js" module are jest mock functions// so we can call .mockImplementation(...) on them// and make assertions like .toHaveBeenCalledTimes(...)
Additionally, if you’d like to mock only parts of a module, you can provide your own “mock module getter” function:
jest.mock('../math', () => {const actualMath = jest.requireActual('../math')return {...actualMath,subtract: jest.fn(),}})// now the `add` export is the normal function,// but the `subtract` export is a mock function.
To learn a bit about how this works, take a look at my repo how-jest-mocking-works. It’s pretty fascinating.
There’s a lot more to learn about the things you can do with Jest’s module mocking capabilities. You can also read the docs about this here:
We’ve got a Location
component that will request the user’s location and then
display the latitude and longitude values on screen. And yup, you guessed it,
window.navigator.geolocation.getCurrentPosition
is not supported by jsdom, so
we need to mock it out. We’ll mock it with a jest mock function so we can call
mockImplementation
and mock what that function does for a particular test.
We’ll also bump into one of the few situations you need to use
act
directly.
Learn more.
Sometimes, the module is interacting with browser APIs that are just too hard to
mock (like canvas
) or you’re comfortable relying on the module’s own test
suite to give you confidence that so long as you use the module properly
everything should work.
In that case, it’s reasonable to mock the module directly. So for this extra credit, try to mock the module rather than the browser API it’s using.
💰 tip, you’re mocking a hook. Your mock implementation can also be a hook (so
you can use React.useState
!).
NOTE: A recording of me doing this extra credit is not on EpicReact.Dev yet, but feel free to give it a try anyway!
Add a test for what happens in the event of an error. You can try it with the module mocking approach, but in my solution, I go back to the function mocking version.
After the instruction, if you want to remember what you’ve just learned, then fill out the elaboration and feedback form:
import React from 'react'import {render, screen, act} from '@testing-library/react'import Location from '../../examples/location'beforeAll(() => {window.navigator.geolocation = {getCurrentPosition: jest.fn(),}})function deferred() {let resolve, rejectconst promise = new Promise((res, rej) => {resolve = resreject = rej})return {promise, resolve, reject}}test('displays the users current location', async () => {const fakePosition = {coords: {latitude: 35,longitude: 139,},}const {promise, resolve} = deferred()window.navigator.geolocation.getCurrentPosition.mockImplementation(callback => {promise.then(() => callback(fakePosition))},)render(<Location />)expect(screen.getByLabelText(/loading/i)).toBeInTheDocument()await act(async () => {resolve()await promise})expect(screen.queryByLabelText(/loading/i)).not.toBeInTheDocument()expect(screen.getByText(/latitude/i)).toHaveTextContent(`Latitude: ${fakePosition.coords.latitude}`,)expect(screen.getByText(/longitude/i)).toHaveTextContent(`Longitude: ${fakePosition.coords.longitude}`,)})test('displays error message when geolocation is not supported', async () => {const fakeError = new Error('Geolocation is not supported or permission denied',)const {promise, reject} = deferred()window.navigator.geolocation.getCurrentPosition.mockImplementation((successCallback, errorCallback) => {promise.catch(() => errorCallback(fakeError))},)render(<Location />)expect(screen.getByLabelText(/loading/i)).toBeInTheDocument()await act(async () => {reject()})expect(screen.queryByLabelText(/loading/i)).not.toBeInTheDocument()expect(screen.getByRole('alert')).toHaveTextContent(fakeError.message)})
Quick Links
Legal Stuff
Social Media