'Test react custom hook with mocking blob response

I have a created a hook as following:

import { AxiosResponse } from 'axios';
import { DEFAULT_EXPORT_FILE_NAME } from 'constants';
import { useRef, useState } from 'react';

export interface ExportFileProps {
  readonly apiDefinition: () => Promise<AxiosResponse<Blob | string>>;
  readonly preExport: () => void;
  readonly postExport: () => void;
  readonly onError: () => void;
}

export interface ExportedFileInfo {
  readonly exportHandler: () => Promise<void>;
  readonly ref: React.MutableRefObject<HTMLAnchorElement | null>;
  readonly name: string | undefined;
  readonly url: string | undefined;
}

export const useExportFile = ({
  apiDefinition,
  preExport,
  postExport,
  onError,
}: ExportFileProps): ExportedFileInfo => {
  const ref = useRef<HTMLAnchorElement | null>(null);
  const [url, setFileUrl] = useState<string>();
  const [name, setFileName] = useState<string>();

  const exportHandler = async () => {
    try {
      preExport();
      const response = await apiDefinition();
      const objectURL = URL.createObjectURL(
        new Blob([response.data], { type: response.headers['content-type'] }),
      );
      setFileUrl(objectURL);
      const fileName =
        response.headers['content-disposition'].match(/filename="(.+)"/)[1] ??
        DEFAULT_EXPORT_FILE_NAME;
      setFileName(fileName);
      ref.current?.click();
      postExport();
      if (url) URL.revokeObjectURL(url);
    } catch (error) {
      onError();
    }
  };

  return { exportHandler, ref, url, name };
};

This hook is used by this compoenent:

interface ExportFileProps {
  readonly apiDefinition: () => Promise<AxiosResponse<Blob | string>>;
}

const ExportFile: React.FC<ExportFileProps> = (props: ExportFileProps): JSX.Element => {
  const { apiDefinition } = props;
  const [buttonState, setButtonState] = useState<ExportButtonState>(ExportButtonState.Primary);

  const preExport = () => setButtonState('loading');
  const postExport = () => setButtonState('primary');


  const onErrorDownloadFile = () => {
    setButtonState('primary');
  };

  const { ref, url, exportHandler, name } = useExportFile({
    apiDefinition,
    preExport,
    postExport,
    onError: onErrorDownloadFile,
  });

  return (
    <div>
      <Box sx={{ display: 'none' }}>
        <a href={url} download={name} ref={ref}>
          &nbsp;
        </a>
      </Box>
      <ExportButton
        clickHandler={exportHandler}
        buttonState={buttonState}
      >
        Download XLS
      </ExportButton>
    </div>
  );
};

export default ExportFile;

So what this hook does is it creates an anchor element and click it to download the blob response from apiDefinition.

What I'm trying to do is to test this hook, so I need to mock a response with a blob file with headers['content-disposition'] and response.headers['content-type'] defined.

And then test the returned value of exportHandler, ref, url and name.

This is what I tried:

import { renderHook } from '@testing-library/react-hooks';
import { useExportFile } from './useExportFile';

describe('useExportFile', () => {
  const apiDefinition = jest.fn();
  const preExport = jest.fn();
  const postExport = jest.fn();
  const onError = jest.fn();

  test('is initialized', async () => {
    const { result } = renderHook(() =>
      useExportFile({
        apiDefinition,
        preExport,
        postExport,
        onError,
      }),
    );

    const exportHandler = result?.current?.exportHandler;
    expect(typeof exportHandler).toBe('function');
  });
});

The issue I'm having is that I don't know how to mock the api call in this case, since it should return a blob with headers defined.

How can I solve this ?



Solution 1:[1]

You can try mocking apiDefinition as below:

const blob: Blob = new Blob(['']);

const axiosResponse: AxiosResponse = {
  data: blob,
  status: 200,
  statusText: "OK",
  config: {},
  headers: {
    "content-disposition": 'filename="some-file"',
    "content-type": "application/json"
  }
};

const apiDefinition = () => Promise.resolve(axiosResponse);

Test Code:

import { act } from 'react-dom/test-utils';
import { AxiosResponse } from 'axios';
import { render } from '@testing-library/react'
import { renderHook } from '@testing-library/react-hooks';
import { useExportFile } from './Export';

describe('useExportFile', () => {
  const FILE_NAME = "some-file"
  const URL = "some-url";
  const blob: Blob = new Blob(['']);

  const axiosResponse: AxiosResponse = {
    data: blob,
    status: 200,
    statusText: "OK",
    config: {},
    headers: {
      "content-disposition": `filename="${FILE_NAME}"`,
      "content-type": "text"
    }
  };

  global.URL.createObjectURL = () => URL;
  global.URL.revokeObjectURL = () => null;
  const onBtnClick = jest.fn();
  const apiDefinition = () => Promise.resolve(axiosResponse);
  const preExport = jest.fn();
  const postExport = jest.fn();
  const onError = jest.fn();

  test('is initialized', async () => {
    const { result } = renderHook(() =>
      useExportFile({
        apiDefinition,
        preExport,
        postExport,
        onError,
      }),
    );

    const container = render(<a href={url} ref={ref} onClick={onBtnClick}></a>);
    const exportHandler = result?.current?.exportHandler;
    expect(typeof exportHandler).toBe('function');
    await act(async () => {
      await exportHandler();
    });
    expect(result?.current?.url).toEqual(URL);
    expect(result?.current?.name).toEqual(FILE_NAME);
    expect(result?.current?.ref?.current).toEqual(await container.findByRole("anchor-btn"))
    expect(onBtnClick).toHaveBeenCalled();
  });
});

Solution 2:[2]

Since the code is async I would suggest testing it with an async mock of Axios like this one: https://www.npmjs.com/package/axios-mock-adapter

This way the test will be closer to the real world.

You will have to implement the ApiDefinition interface but the test will be more real. Also, you can add delays with such an approach and test retries/timeouts if needed.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Mihail Vratchanski