Codementor Events

Fetching Data In Jest Tests

Published Jan 09, 2025
Fetching Data In Jest Tests

You can't (and shouldn't) fetch data from the server in tests. Every test should work with static or predefined sets of data.

This is often not easy and requires a certain amount of code mocking—mock a function, utility, or Redux action that calls the server. At some point test files start getting ugly with a bunch of definitions like these:

import React from "react";

jest.mock("../store/actions/loadInvoices", () => ({
  __esModule: true,
  loadInvoices: jest.fn().mockResolvedValue([
    {
      id: "1",
      text: "Invoice text"
    },
    {
      id: "2",
      text: "Invoice text"
    }
  ]),
}));

jest.mock("../store/actions/openModal", () => ({
  __esModule: true,
  openModal: jest.fn().mockReturnValue(jest.fn()),
}));

jest.mock("../lib/hooks/useMutation.js", () => ({
  useMutation: jest.fn(),
}));

// and so on...

Working this way becomes especially cumbersome with integration tests where value comes from penetrating the whole system. Which won't work with mocks. A request needs to go to the server but since there is no server in Jest env, it should at least go closer. The closest call is window.fetch().

Now let's imagine tests with mock-free code:

import React from "react";

describe("InvoicesTable", () => {
  it("should load all invoices for a domain", () => {
    // Return specific data for
    // an endpoint and request method (GET)
    server.get("/api/v2/invoices/domainname.com", [
      {
        id: "1",
        text: "Invoice text 1"
      },
      {
        id: "2",
        text: "Invoice text 2"
      }
    ]);
    
    render(<InvoicesTable domain="domainname.com" />);
    
    expect(screen.queryByText("Invoice text 1")).toBeInTheDocument();
    expect(screen.queryByText("Invoice text 2")).toBeInTheDocument();
  });
});

Looks much more natural and specific to the test case. So instead of mocking actions (loadInvoices) it's better to describe the request ergonomically as "if there is a real server".

Here is the "server" API:

// More about "Fetch" below
const server = new Fetch();

server.get("URL", response);  // GET request
server.post("URL", response);  // POST request
server.delete("URL", response);  // POST request
server.put("URL").reject(response);  // PUT request that fails

Obviously, the only thing to be mocked globally is the window.fetch() function. This can be done with a Fetch util class and used across the whole test suite. No external dependencies are needed.

// [url, response, [isRejected]]
const defaultResponses = {
  get: [],
  put: [],
  post: [],
  patch: [],
  delete: [],
};

export default class Fetch {
  constructor() {
    // Steal (mock) "fetch" and assign custom function
    global.fetch = (url, options) => {
      let response;
      let isOk = true;
      const requestMethod = options.method
        ? options.method.toLowerCase()
        : "get";

      // Find the index of a response for a specific URL (endpoint)
      // url = "/api/v2/resource/email@site.com"
      const index = this.responses[requestMethod].findIndex(
        ([path]) => path === url,
      );

      // If server.get("/api/v2/resource/email@site.com", response) has been
      // called at some point gets its corresponding response.
      if (index >= 0) {
        response = this.responses[requestMethod][index][1];
        // should reject?
        isOk = !this.responses[requestMethod][index][2];
      }
      return Promise.resolve({
        ok: isOk,
        json: () => Promise.resolve(response),
      });
    };
  }

  responses = defaultResponses;
  fetch = global.fetch;
  latest = null;

  request(url, method, response) {
    let index = this.responses[method].findIndex(([path]) => path === url);

    if (index < 0) {
      // server.get("/api/v2/resource/email@site.com", { data: [] })
      // this.responses["GET"].push(["/api/v2/resource/email@site.com", { data: [] }])
      this.responses[method].push([url, response]);
      index = this.responses[method].length - 1;
    } else {
      // Calling server.get("/api/v2/resource/email@site.com", response)
      // with two different responses will record the second one only
      this.responses[method][index] = [url, response];
    }
    this.latest = this.responses[method][index];

    return this;
  }

  get(url, response) {
    return this.request(url, "get", response);
  }
  put(url, response) {
    return this.request(url, "put", response);
  }
  post(url, response) {
    return this.request(url, "post", response);
  }
  patch(url, response) {
    return this.request(url, "patch", response);
  }
  delete(url, response) {
    return this.request(url, "delete", response);
  }

  reject(response) {
    if (this.latest) {
      this.latest[1] = response;
      this.latest[2] = true;
    }
  }

  restore() {
    global.fetch = this.fetch;
    this.responses = defaultResponses;
  }

  clear() {
    this.responses = defaultResponses;
    this.latest = null;
  }
}

Use it directly in your test util lib with some React Testing Library exports:

// /lib/tests/index.js

import Fetch from "./Fetch";
import "@testing-library/jest-dom";
import { render, screen, fireEvent } from "@testing-library/react";

export {
  render,
  screen, 
  fireEvent,
  server: new Fetch()
}
Discover and read more posts from Miroslav Nikolov
get started