Simple fetch mock using Typescript and Jest
Asked Answered
G

5

44

What would be my absolute easiest way of mocking fetch using Typescript?

I would just like to do something simple like below. But Typescript tells me that I'm not matching the definition for the full fetch object.

Type 'Mock<Promise<{ json: () => Promise<{ test: number; }>; }>, []>' is not assignable to type '(input: RequestInfo, init?: RequestInit | undefined) => Promise<Response>'.
   Type 'Promise<{ json: () => Promise<{ test: number; }>; }>' is not assignable to type 'Promise<Response>'.
     Type '{ json: () => Promise<{ test: number; }>; }' is missing the following properties from type 'Response': headers, ok, redirected, status, and 11 more.

What would be the simplest solution to get around this? Actually mock out the whole fetch object or other solution?

global.fetch = jest.fn(() =>
  Promise.resolve({
    json: () => Promise.resolve({ test: 100 }),
  }),
)
Glarum answered 13/11, 2020 at 9:18 Comment(2)
What exactly does "TypeScript goes crazy" mean? Presumably an error that your test double doesn't have the same interface as the actual fetch. Have you considered a type assertion?Joeljoela
TS tells me that I'm not matching the full definition for fetch - that's fine. @Joeljoela can typer assertion get me around that? Type 'Mock<Promise<{ json: () => Promise<{ test: number; }>; }>, []>' is not assignable to type '(input: RequestInfo, init?: RequestInit | undefined) => Promise<Response>'. Type 'Promise<{ json: () => Promise<{ test: number; }>; }>' is not assignable to type 'Promise<Response>'. Type '{ json: () => Promise<{ test: number; }>; }' is missing the following properties from type 'Response': headers, ok, redirected, status, and 11 more.Glarum
C
115

You can tell TypeScript that you're defining global.fetch as a Jest mock.

global.fetch = jest.fn(() =>
  Promise.resolve({
    json: () => Promise.resolve({ test: 100 }),
  }),
) as jest.Mock;
Cila answered 13/11, 2020 at 10:47 Comment(4)
man I wasted 2 days on this, Thanks...Akela
you can also use this, which is similar: jest.spyOn(global, "fetch").mockImplementation( jest.fn(() => Promise.resolve({ json: () => Promise.resolve({ data: 100 }), }), ) as jest.Mock )Ample
@GreatQuestion I feel yours is a better implementation using spyOn. I don't know why but using spyOn gives much less errors undefined errors than simply using jest.fn(). It would have been better if you posted it as a separate answer.Pagepageant
if i running other tests does it impact them also? or it just bound to the test scope?Nonesuch
F
13

I had some problems using the before approaches, here is how I work around that:

First my test code:

describe("Testing the Assets Service", () => {
let fetchMock: any = undefined;

beforeEach(() => {
    fetchMock = jest.spyOn(global, "fetch")
    .mockImplementation(assetsFetchMock);
});

afterEach(() => {
    jest.restoreAllMocks();
});

test('Fetch has been called', () => {
    const baseUrl = "https://myurl.com"
    fetchAssets(baseUrl);
    expect(fetchMock).toHaveBeenCalled();
    expect(fetchMock).toHaveBeenCalledWith(baseUrl);
}); 
});

The function fetchAssets call the fetch function with an specific url.

Now the function that mocks the fetch behavior:

export const assetsFetchMock = () => Promise.resolve({
ok: true,
status: 200,
json: async () => clientAssets
} as Response);

clientAssets is a object that I needed to return, you could replace it to the object or primitive you have to return.

Followthrough answered 19/10, 2022 at 16:27 Comment(0)
D
11

you can also use this, which is similar to this answer https://mcmap.net/q/369526/-simple-fetch-mock-using-typescript-and-jest

jest.spyOn(global, "fetch").mockImplementation( 
  jest.fn(
    () => Promise.resolve({ json: () => Promise.resolve({ data: 100 }), 
  }), 
) as jest.Mock ) 

from this comment Simple fetch mock using Typescript and Jest

Ddt answered 15/7, 2022 at 4:4 Comment(1)
I've been attempting to get this working all morning, and this was what did it for me! Thank you so much 🙌Dewayne
A
1

Worked on a fetch resolver that can be initialised for each test case. Hope this is useful. You can add custom methods like based on the need.

type Method = "get" | "options" | "post" | "put" | "patch" | "delete";

// https://httpstat.us
export enum Status {
  OK = 200,
  Created = 201,
  Accepted = 202,
  NonAuthoritativeInformation = 203,
  NoContent = 204,
  ResetContent = 205,
  PartialContent = 206,
  MultipleChoices = 300,
  MovedPermanently = 301,
  Found = 302,
  SeeOther = 303,
  NotModified = 304,
  UseProxy = 305,
  Unused = 306,
  TemporaryRedirect = 307,
  PermanentRedirect = 308,
  BadRequest = 400,
  Unauthorized = 401,
  PaymentRequired = 402,
  Forbidden = 403,
  NotFound = 404,
  MethodNotAllowed = 405,
  NotAcceptable = 406,
  ProxyAuthenticationRequired = 407,
  RequestTimeout = 408,
  Conflict = 409,
  Gone = 410,
  LengthRequired = 411,
  PreconditionFailed = 412,
  RequestEntityTooLarge = 413,
  RequestURITooLong = 414,
  UnsupportedMediaType = 415,
  RequestedRangeNotSatisfiable = 416,
  ExpectationFailed = 417,
  Imateapot = 418,
  MisdirectedRequest = 421,
  UnprocessableEntity = 422,
  Locked = 423,
  TooEarly = 425,
  UpgradeRequired = 426,
  PreconditionRequired = 428,
  TooManyRequests = 429,
  RequestHeaderFieldsTooLarge = 431,
  UnavailableForLegalReasons = 451,
  InternalServerError = 500,
  NotImplemented = 501,
  BadGateway = 502,
  ServiceUnavailable = 503,
  GatewayTimeout = 504,
  HTTPVersionNotSupported = 505,
  VariantAlsoNegotiates = 506,
  InsufficientStorage = 507,
  NetworkAuthenticationRequired = 511,
  Webserverisreturninganunknownerror = 520,
  Connectiontimedout = 522,
  Atimeoutoccurred = 524
}

/**
 * Stub API request, response in test cases.
 * - should be initialized and destroyed within the context of a specific case.
 * - highly customizable
 *
 * <pre>
 *  describe("Fetch API", () => {
 *    let fetchResolver!: FetchResolver;
 *      beforeEach(() => {
 *      fetchResolver = new FetchResolver();
 *    });
 *
 *    it("should load api", async () => {
 *      // stub
 *      fetchResolver.stub( "http://localhost:8080/endpoint", "post", { id: 100 }, { created: true }, 200);
 *      // fetch
 *      fetch("http://localhost:8080/endpoint",
 *        { method: "post", body: JSON.stringify({ id: 100 })}
 *      ).then((response) => {
 *        if (response.ok) {
 *          response.text().then((text) => {
 *            console.log(text); // { created: true }
 *            expect(text).toBeEqual({ created: true });
 *          });
 *        }
 *      });
 *    });
 *
 *    afterEach(() => {
 *      fetchResolver.clear();
 *    });
 *  });
 * </pre>
 *
 * Even though jest executes tests in parallel jest instance,
 * We can't go wrong if stubs are cleaned after its use
 */
export class FetchResolver {
  private mocks: Map<string, Response> = new Map();
  constructor() {
    this.init();
  }

  public stub(
    uri: string,
    method: Method,
    payload: any,
    response: any,
    status: Status
  ) {
    const finalRequest: { input: RequestInfo | URL; init?: RequestInit } = {
      input: uri,
      init: {
        method: method,
        body: JSON.stringify(payload)
      }
    };
    console.log(
      `mocking fetch :::\nrequest ${this.prettyPrint(
        finalRequest
      )} with \nresponse ${this.prettyPrint(response)} ans status ${status}`
    );
    this.mocks.set(
      JSON.stringify(finalRequest),
      new Response(JSON.stringify(response), { status: status })
    );
  }

  private prettyPrint(json: any) {
    return JSON.stringify(json, null, 2);
  }

  public clear() {
    this.mocks.clear();
  }

  private init() {
    jest
      .spyOn(global, "fetch")
      .mockImplementation((input: RequestInfo | URL, init?: RequestInit) => {
        const request = {
          input,
          init
        };
        return new Promise((resolve, reject) => {
          let response = this.mocks.get(JSON.stringify(request));
          if (response) {
            resolve(response);
          } else {
            // rejecting here will hurt component initialization
            console.error(
              `mock not implemented :::\nrequest ${this.prettyPrint(request)}`
            );
            // return empty response
            resolve(new Response("{}"));
          }
        });
      });
  }
  public static initialize() {
    let resolver = new FetchResolver();
    resolver.stub(
      "http://localhost:8080/endpoint",
      "post",
      { id: 100 },
      {
        created: true
      },
      200
    );
    fetch("http://localhost:8080/endpoint", {
      method: "post",
      body: JSON.stringify({ id: 100 })
    }).then((response) => {
      if (response.ok) {
        response.text().then((text) => {
          console.log(text); // { created: true }
        });
      }
    });
  }
}

https://gist.github.com/mukundhan94/102334a8ba9b84d93a1b5ba4b7838647

Aintab answered 21/9, 2023 at 12:56 Comment(1)
This answer was a great starting point for my purposes of having a flexible fetch mocker with stubber functionality.Quinonez
A
0

In my case I need to mock a request for a table and I use vitest.

mock:

const mockData = {
  total: 20,
  attendees: [
    {
      id: "1",
      name: "John Doe",
      email: "[email protected]",
      createdAt: "2024-04-02T00:00:00.000Z",
      checkedInAt: null,
    },
    {
      id: "2",
      name: "Jane Smith",
      email: "[email protected]",
      createdAt: "2024-04-01T00:00:00.000Z",
      checkedInAt: "2024-04-03T10:00:00.000Z",
    },
  ],
};

// @ts-ignore
global.fetch = vi.fn(() =>
  Promise.resolve({
    json: () => Promise.resolve(mockData),
  })
);

test:

describe("Attendee List", () => {
  it("Renders attendee list header, table, and pagination", async () => {
    render(<AttendeeList />);

    expect(screen.getByText("Participantes")).toBeTruthy();
    expect(screen.getByText("Código")).toBeTruthy();
    expect(screen.getByText("Participante")).toBeTruthy();
    expect(screen.getByText("Data da inscrição")).toBeTruthy();
    expect(screen.getByText("Data do check-in")).toBeTruthy();

    await waitFor(() => {
      expect(screen.getByText("John Doe")).toBeTruthy();
      expect(screen.getByText("[email protected]")).toBeTruthy();
      expect(screen.getByText("Não faz check-in")).toBeTruthy();
      expect(screen.getByText("Página 1 de 2")).toBeTruthy();
    });
  });
});

request that I call inside my application:

const url = new URL(
      "http://localhost:3333/events/9e9bd979-9d10-4915-b339-3786b1634f33/attendees"
    );

    url.searchParams.set("pageIndex", String(page - 1));
    if (search.length > 0) {
      url.searchParams.set("query", search);
    }

    fetch(url)
      .then((resp) => resp.json())
      .then((data) => {
        setTotal(data.total);
        setAttendees(data.attendees);
      });
Algerian answered 4/4, 2024 at 0:45 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.