测试反应自定义挂钩与模拟 blob 响应
Test react custom hook with mocking blob response
我创建了一个钩子,如下所示:
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 };
};
这个钩子被这个组件使用:
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}>
</a>
</Box>
<ExportButton
clickHandler={exportHandler}
buttonState={buttonState}
>
Download XLS
</ExportButton>
</div>
);
};
export default ExportFile;
所以这个挂钩的作用是创建一个锚元素并单击它从 apiDefinition
下载 blob 响应。
我想做的是测试这个钩子,所以我需要用一个定义了 headers['content-disposition']
和 response.headers['content-type']
的 blob 文件来模拟响应。
然后测试 exportHandler
、ref
、url
和 name
的 returned 值。
这是我试过的:
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');
});
});
我遇到的问题是在这种情况下我不知道如何模拟 api 调用,因为它应该 return 一个定义了 headers 的 blob。
我该如何解决这个问题?
您可以尝试如下模拟 apiDefinition
:
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);
测试代码:
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();
});
});
由于代码是异步的,我建议使用像这样的 Axios 异步模拟来测试它:https://www.npmjs.com/package/axios-mock-adapter
这样测试会更接近真实世界。
您将必须实现 ApiDefinition 接口,但测试会更加真实。此外,您可以使用这种方法添加延迟并在需要时测试 retries/timeouts。
我创建了一个钩子,如下所示:
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 };
};
这个钩子被这个组件使用:
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}>
</a>
</Box>
<ExportButton
clickHandler={exportHandler}
buttonState={buttonState}
>
Download XLS
</ExportButton>
</div>
);
};
export default ExportFile;
所以这个挂钩的作用是创建一个锚元素并单击它从 apiDefinition
下载 blob 响应。
我想做的是测试这个钩子,所以我需要用一个定义了 headers['content-disposition']
和 response.headers['content-type']
的 blob 文件来模拟响应。
然后测试 exportHandler
、ref
、url
和 name
的 returned 值。
这是我试过的:
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');
});
});
我遇到的问题是在这种情况下我不知道如何模拟 api 调用,因为它应该 return 一个定义了 headers 的 blob。
我该如何解决这个问题?
您可以尝试如下模拟 apiDefinition
:
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);
测试代码:
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();
});
});
由于代码是异步的,我建议使用像这样的 Axios 异步模拟来测试它:https://www.npmjs.com/package/axios-mock-adapter
这样测试会更接近真实世界。
您将必须实现 ApiDefinition 接口,但测试会更加真实。此外,您可以使用这种方法添加延迟并在需要时测试 retries/timeouts。