Notes on mocking fetch and Response in Jest
Being able to test your application in different API scenarios can be like a superpower.
And while Jest can support you along the way, you may still find yourself needing to mock fetch
and Response
to simulate your APIs.
The good news is that fetch
is pretty easy to mock. After all, fetch
is nothing but a Promise<Response>
.
The issue is with Response
. In fact, as Jest works in Node.js you cannot use the Response
class as you would in the browser.
This is because Node.js does not support the Web APIs out of the box (no fetch
and no Response
) and so you’ll need a polyfill to mock your Response
.
To do so, you can use the node-fetch
package which contains a Response
class, and if you use TypeScript, you can use the @types/node-fetch
package to enable types support.
/*
NOTES:
Let's pretend that doAPICall is the function that
performs the fetch to your API and manipulate the result
function doAPICall() {
return fetch(url, options)
.catch(() => fetch(url, options)) // retry
.then(r => r.ok ? r.json() : {});
}
You would use this as:
doAPICall()
.then(...)
.catch(...)
*/
import { doAPICall } from "./doAPICall";
import { Response } from "node-fetch";
// pass response to doAPICall if needed
Object.defineProperty(
global.self,
"Response",
{ value: Response }
);
const data = {test: "test"};
describe("API Response Handler", () => {
test("case: 200 positive response", async () => {
const resp = new Response(JSON.stringify(data), {status: 200});
global.fetch = jest.fn().mockResolvedValue(resp);
const data = await doAPICall();
expect(data).toStrictEqual({"test": "test"});
});
test("case: 404 positive response", async () => {
const resp = new Response("Page not Found :(", {status: 404});
global.fetch = jest.fn().mockResolvedValue(resp);
const data = await doAPICall();
expect(data).toStrictEqual({});
});
test("case: network error", async () => {
const error = new TypeError("Failed to fetch");
global.fetch = jest.fn().mockRejectedValue(error);
const data = await doAPICall();
expect(data).toStrictEqual({});
});
test("case: network error and retry", async () => {
const error = new TypeError("Failed to fetch");
const resp = new Response(JSON.stringify(data), {status: 200});
global.fetch =
jest.fn()
.mockImplementationOnce(() => Promise.reject(error))
.mockImplementationOnce(() => Promise.resolve(resp));
const data = await doAPICall();
expect(data).toStrictEqual({"test": "test"});
});
});
Troubleshooting
ESM / CJS Issue
Jest encountered an unexpected token
.
.
.
./node_modules/node-fetch/src/index.js:9
import http from 'node:http';
SyntaxError: Cannot use import statement outside a module
In case of errors with the import statement, use npm i -D node-fetch@2
.
By default, the latest version of node-fetch
comes only as ESM
. If you need the CommonJS
, please use node-fetch@2
.
Mocking HTTP status codes that indicate errors
According to MDN fetch docs:
A fetch() promise only rejects when the request fails, for example, because of a badly-formed request URL or a network error. A fetch() promise does not reject if the server responds with HTTP status codes that indicate errors (404, 504, etc.). Instead, a then() handler must check the Response.ok and/or Response.status properties.
So, fetch
rejects only when the request fails. Cases: badly-formed request URL or a network error.