FE / ts / js

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.

Next