GAZAR

Principal Engineer | Mentor

How to test react hooks

How to test react hooks

I have a fairly complicated hooks here

import IProduct, { productSchema } from "../../prisma/types/IProduct";
import { useState } from "react";
interface ResponseType {
  message: string;
  data: Record<string, string>;
}

const useProductsHandler = (defaultProducts: IProduct[] = []) => {
  const [products, setProducts] = useState<IProduct[]>(defaultProducts);
  const initFromCSV = (data: string[][]): string[] => {
    const errors: string[] = [];
    const newProducts = data
      .filter((row: string[]) => {
        const exist = products.find(
          (product) => product.sku.trim() === row[1].trim()
        );
        if (exist) errors.push(exist.sku);
        return !exist;
      })
      .map((row: string[]) => {
        const product: IProduct = {
          quantity: Number(row[0].trim()),
          sku: row[1].trim(),
          oldSku: row[1].trim(),
          description: row[2].trim(),
          store: row[3].trim(),
          isSaved: false,
        };
        productSchema.validate(product, { abortEarly: false }).catch((err) => {
          product.errors = err.errors;
        });
        return product;
      });
    setProducts([...products, ...newProducts]);
    return errors;
  };

  const initFromAPI = (data: IProduct[]): void => {
    const newProducts = data.map((product) => {
      product.isSaved = true;
      return product;
    });
    setProducts(newProducts);
  };

  const ifSkusExists = (sku: string): boolean => {
    const exist: IProduct | undefined = products.find(
      (product) => product.sku.trim() === sku.trim()
    );
    return !!exist;
  };

  const update = (sku: string, data: IProduct): void => {
    const newProducts = products.map((product: IProduct) => {
      if (product.sku === sku) {
        data.isSaved = false;
        data.oldSku = product.sku;
        return data;
      }
      return product;
    });
    setProducts(newProducts);
  };

  const remove = (sku: string): void => {
    const newProducts = products.filter((product: IProduct) => {
      return product.sku !== sku;
    });
    setProducts(newProducts);
  };

  const clear = (): void => {
    setProducts([]);
  };

  const saveAll = (response: ResponseType): void => {
    const { data } = response;
    products.forEach((product) => {
      if (data[product.sku] === "created" || data[product.sku] === "updated") {
        product.isSaved = true;
      } else if (data[product.sku] === "error") {
        product.isSaved = false;
        product.errors = [data[product.sku]];
      }
    });
  };

  const getToSaveProducts = (): IProduct[] => {
    const newProducts: IProduct[] = [];
    products.forEach((product) => {
      if (!product.isSaved) {
        newProducts.push({
          sku: product.sku,
          oldSku: product.oldSku,
          description: product.description,
          quantity: product.quantity,
          store: product.store,
        });
      }
    });
    return newProducts;
  };

  const add = (data: IProduct) => {
    let error = null;
    const exist = products.find(
      (product) => product.sku.trim() === data.sku.trim()
    );

    if (exist) {
      error = "SKU already exists";
      return error;
    }

    data.isSaved = false;
    data.oldSku = data.sku;
    setProducts([...products, data]);
    return error;
  };

  return {
    products,
    initFromCSV,
    initFromAPI,
    update,
    remove,
    clear,
    add,
    ifSkusExists,
    saveAll,
    getToSaveProducts,
  };
};

Make sure you have jest installed, and edit your tsconfig.ts

"types": ["@remix-run/node", "vite/client", "jest"],

then you need ts-jest and babel-jest with your jest.config.ts

module.exports = {
  preset: "ts-jest",
  testEnvironment: "jsdom",
  transform: {
    "^.+\\.(ts|tsx)?$": "ts-jest",
    "^.+\\.(js|jsx)$": "babel-jest",
  },
  verbose: true,
};

And in order to test it, I needed to first install `@testing-library/react` and then Mock my prisma layer

import { act } from "react-dom/test-utils";
import useProductsHandler from "../useProductsHandler";
import { renderHook } from "@testing-library/react";

jest.mock("../../../prisma/types/IProduct", () => ({
  productSchema: {
    validate: jest.fn().mockReturnValue({
      catch: jest.fn().mockResolvedValue({ errors: [] }),
    }),
  },
}));
export default useProductsHandler;

Then the first test could be as easy as:

test("initFromCSV to be working", async () => {
  const { result } = renderHook(() => useProductsHandler());

  const { initFromCSV } = result.current;
  act(() => {
    const errors = initFromCSV([["1", "SK-123", "desc", "store"]]);
    expect(errors).toStrictEqual([]);
  });
});

and the rest would be similar

test("initFromAPI returns errors", async () => {
  const { result } = renderHook(() =>
    useProductsHandler([
      {
        quantity: 1,
        sku: "123",
        oldSku: "123",
        description: "desc",
        store: "store",
        isSaved: false,
      },
    ])
  );

  const { initFromCSV } = result.current;

  act(() => {
    const errors = initFromCSV([["1", "123", "desc", "store"]]);
    expect(errors).toStrictEqual(["123"]);
  });
});

test("initFromAPI to be working", async () => {
  const { result } = renderHook(() => useProductsHandler());

  const { initFromAPI } = result.current;

  act(() => {
    initFromAPI([
      {
        quantity: 1,
        sku: "123",
        oldSku: "123",
        description: "desc",
        store: "store",
        isSaved: false,
      },
    ]);
  });

  expect(result.current.products).toStrictEqual([
    {
      quantity: 1,
      sku: "123",
      oldSku: "123",
      description: "desc",
      store: "store",
      isSaved: true,
    },
  ]);
});

test("ifSkusExists to be working", async () => {
  const { result } = renderHook(() =>
    useProductsHandler([
      {
        quantity: 1,
        sku: "SK-123",
        oldSku: "SK-123",
        description: "desc",
        store: "store",
        isSaved: false,
      },
    ])
  );

  const { ifSkusExists } = result.current;

  act(() => {
    const exists = ifSkusExists("SK-123");
    expect(exists).toBe(true);
  });
});

test("update to be working", async () => {
  const { result } = renderHook(() =>
    useProductsHandler([
      {
        quantity: 1,
        sku: "123",
        oldSku: "123",
        description: "desc",
        store: "store",
        isSaved: false,
      },
    ])
  );

  const { update } = result.current;

  act(() => {
    update("123", {
      quantity: 2,
      sku: "123",
      oldSku: "123",
      description: "desc",
      store: "store",
      isSaved: false,
    });
  });

  expect(result.current.products).toStrictEqual([
    {
      quantity: 2,
      sku: "123",
      oldSku: "123",
      description: "desc",
      store: "store",
      isSaved: false,
    },
  ]);
});

test("getToSaveProducts to be working", async () => {
  const { result } = renderHook(() =>
    useProductsHandler([
      {
        quantity: 1,
        sku: "123",
        oldSku: "123",
        description: "desc",
        store: "store",
        isSaved: false,
      },
    ])
  );

  const { getToSaveProducts } = result.current;

  act(() => {
    const toSave = getToSaveProducts();
    expect(toSave).toStrictEqual([
      {
        quantity: 1,
        sku: "123",
        oldSku: "123",
        description: "desc",
        store: "store",
      },
    ]);
  });
});

test("getToSaveProducts to be working but return nothing", async () => {
  const { result } = renderHook(() =>
    useProductsHandler([
      {
        quantity: 1,
        sku: "123",
        oldSku: "123",
        description: "desc",
        store: "store",
        isSaved: true,
      },
    ])
  );

  const { getToSaveProducts } = result.current;

  act(() => {
    const toSave = getToSaveProducts();
    expect(toSave).toStrictEqual([]);
  });
});

test("add to be working", async () => {
  const { result } = renderHook(() => useProductsHandler());

  const { add } = result.current;

  act(() => {
    add({
      quantity: 1,
      sku: "123",
      oldSku: "123",
      description: "desc",
      store: "store",
      isSaved: false,
    });
  });

  expect(result.current.products).toStrictEqual([
    {
      quantity: 1,
      sku: "123",
      oldSku: "123",
      description: "desc",
      store: "store",
      isSaved: false,
    },
  ]);
});


test("add to be working but return error", async () => {
  const { result } = renderHook(() =>
    useProductsHandler([
      {
        quantity: 1,
        sku: "SK-123",
        oldSku: "SK-123",
        description: "desc",
        store: "store",
        isSaved: false,
      },
    ])
  );

  const { add } = result.current;

  act(() => {
    const error = add({
      quantity: 1,
      sku: "SK-123",
      oldSku: "SK-123",
      description: "desc",
      store: "store",
      isSaved: false,
    });

    expect(error).toBe("SKU already exists");
  });

});