It seems that there is no such project that does not need to make requests to the server. And it is always a good idea to write a separate abstraction to describe such communication. This not only adds readability and reusability to the code, but also makes it possible to quickly and efficiently write tests for such functionality.

class NetworkService {
    constructor(private readonly fetch: typeof fetch) {}
    getUser: (): Promise<IUser> => this.fetch('/api/user').then(d => d.json())
}

function getUsername(api: Pick<NetworkService, 'getUser'>) {
    return api.getUser().then(user => user.name);
}

test('getUsername', async () => {
    const name = await getUsername({
        getUser: () => Promise.resolve({ name: 'test' } as IUser)
    });
    expect(name).toBe('test');
});

And for an average project, this approach will be sufficient. But sometimes there are requirements that you need to redefine the fetch itself. For example, in our project, we are developing an extension for the browser and we want to make requests through the serviceWorker. And there is a requirement to reimplement the Fetch API. As we know, fetch returns a Response primitive, for the creation of which you need to pass the ReadableStream as the first argument, and the response characteristics as the second. Thus, we can use the following construction.

function buildFetchEmulator(
  controller: (url: RequestInfo, init: RequestInit | undefined) =>
    Promise<[ResponseInit, (add: (msg: object) => void) => Promise<void>]>
) {
  const cFetch: typeof fetch = async (url, init) => {
    // extract the response characteristics and the controller that will generate the data
    const [responseInit, ctrl] = await controller(url, init);

    // describe the behavior of ReadableStream
    const stream = new ReadableStream({
      async start(controller) {
        try {
          const enc = new TextEncoder();
          await ctrl((msg) => {
            // convert the object to a string
            const message = JSON.stringify(msg);
            // string in Uint8Array
            const buf = enc.encode(message);
            // transfer data to the ReadableStream controller
            controller.enqueue(buf);
          });
          controller.close();
        } catch (err) {
          controller.error(err);
          controller.close();
        }
      },
    });

    return new Response(stream, responseInit);
  };

  return cFetch;
}

test('getUser', () => {
    const customFetch = buildFetchEmulator(async (url, init) => {
        if (url === '/api/user') {
            return [{
                headers: new Headers({ 'content-type': contentType }),
                status: 200,
                statusText: 'OK',
            }, async (add) => {
                // Here we can use any logic for data transport
                add({ name: 'test' })
            }];
        }
        throw new Error('not implemented');
    })
    const network = new NetworkService();

    const name = await getUsername(network);
    expect(name).toBe('test');
});

Here I would like to emphasize once again that for cases when there is no need to emulate fetch and write custom adapters, the first method is much more concise and convenient, but with this approach we can test even external libraries that use fetch directly, for example like @microsoft/fetch-event-source.