使用 Typescript 和测试库在 React 中进行 Axios Mocking
我昨天遇到了一个问题,所以我想写一篇关于我的发现和解决方案的文章。
最后,它真的很简单(我越是卡在某个问题上,解决方案就越容易……🤷♂️)。我的 googlefu 完全失败了,因为我找到的每个解决方案要么过时,要么不完整,或者更重要的是:没有考虑到 Typescript。
这里使用的重要包(github链接在最后):
- @testing-library/jest-dom v5.1.1,
- @testing-library/react v9.4.1
- ts-jest v25.2.1
- jest v25.1.0
- axios v0.19.2
嗯。从哪儿开始呢?我们先从基本的useFetch 钩子开始,因为我们在这里使用 axios 来获取数据。
useFetch axios 钩子
export interface IUseFetch {
response: any;
loading: boolean;
error: boolean;
}
export const useFetch = (run: boolean, url: string) => {
const [response, setResponse] = useState({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
useEffect(() => {
let mounted = true;
const abortController = new AbortController();
const signal = abortController.signal;
if (run && mounted) {
const fetchData = async () => {
try {
setLoading(true);
const response = await axios.get(url);
if (response.status === 200 && !signal.aborted) {
setResponse(response.data);
}
} catch (err) {
if (!signal.aborted) {
setResponse(err);
setError(true);
}
} finally {
if (!signal.aborted) {
setLoading(false);
}
}
};
fetchData();
}
return () => {
mounted = false;
abortController.abort();
};
}, [run, url]);
return { response, loading, error };
}
相当标准的 useFetch hook。run
变量是触发 fetch 运行的触发器。
应用程序
接下来,我们来看看基本的 React 组件。这个组件只是一个输入框,它执行搜索操作,并显示一个 div,其中包含一些来自我们useFetch
上面 hook 的搜索结果。
export interface ILocation {
location: string;
country: string;
}
export default function App() {
const [searchString, setSearchString] = useState("");
const [isPanelOpen, setIsPanelOpen] = useState(false); // show/hide results
const [doSearch, setDoSearch] = useState(false); // controls fetch run
// useFetch hook above.
const { response, loading } = useFetch(doSearch, "test.json");
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchString(e.target.value);
};
// If the searchString length > 0, then do the following
useEffect(() => {
setDoSearch(searchString.length > 0);
setIsPanelOpen(searchString.length > 0);
}, [searchString.length]);
const renderSearchResults = () =>
!loading &&
!error &&
response &&
response.length > 0 && (
<ul aria-label="search-results">
{response.map((loc: ILocation, i: number) => (
<li key={i}>
{loc.location}, {loc.country}
</li>
))}
</ul>
);
return (
<div className="App">
<label htmlFor="search">Search:</label>
<input
type="text"
aria-label="search-input" // label used by our tests
id="search"
name="search"
autoComplete="off"
value={searchString}
onChange={handleChange}
/>
{isPanelOpen && (
<div aria-label="search-panel">{renderSearchResults()}</div>
)}
</div>
);
}
够简单吗?太棒了。
现在进行测试。
测试
在我们开始之前,看看上面的代码,我们有三个 aria 标签可以用来断言。
- search-input:我们的输入框
- search-panel:搜索结果容器 div。根据响应,它可能显示为空(此处不做介绍)。
- search-results:保存来自 useFetch hook 的实际 json 响应
首先,让我们准备测试文件。
创建一个名为的文件App.test.tsx
并进行设置:
import '@testing-library/jest-dom';
import '@testing-library/jest-dom/extend-expect';
import React from 'react';
import {
cleanup,
render,
fireEvent,
wait,
} from '@testing-library/react';
import axios from 'axios';
import App from './App';
jest.mock('axios');
注意事项:
@testing-library/jest-dom/extend-expect
:允许我们对 jest-dom 进行一些有用的扩展,例如.toBeInTheDocument()
。- 我们正常导入 axios 不需要有趣的名字。
- 我们做一个标准
jest.mock('axios')
这让我们的测试知道,每当他们看到一个 axios 导入时,就用一个模拟函数替换它。
你来这里的目的:模拟
在编写测试之前,我们先进行模拟。我们将模拟 axios,这也是我遇到困难的部分。其实很简单。
import { AxiosResponse } from 'axios';
// load our test.json file. This can be copied to the local
// folder. Can be a short version of your actual data set.
const testJson = require('../../test.json');
// Our mocked response
const axiosResponse: AxiosResponse = {
data: testJson,
status: 200,
statusText: 'OK',
config: {},
headers: {},
};
// axios mocked
export default {
// Typescript requires a 'default'
default: {
get: jest.fn().mockImplementation(() => Promise.resolve(axiosResponse)),
},
get: jest.fn(() => Promise.resolve(axiosResponse)),
};
这里发生的事情是,我们创建了一个模拟的 AxiosResponse,它包含所有必需品,例如response.status
我们在 useFetch 钩子中使用的,然后是最重要的部分:response.data
。
然后我们有了实际的 axios mock。每当我们的应用看到 axios import 时,它就会使用其中的内容。我们get
在本例中使用它,所以我添加了一个get
mock。这里需要注意的是,我们有一个,default
并且它是 Typescript 使用的。更多信息请点击此处
测试
接下来,我们编写测试。在这个测试中,我们将遵循Kent C. Dodds 在他的博客中提出的建议。因此,我们只进行一次端到端 (E2E) 测试。这将涵盖用户在输入框中输入内容并查看搜索结果的整个过程。
test("type text into input, and display search results", async () => {
// our test searchString
const searchString = "syd";
// Render App
const { getByLabelText, queryByLabelText, debug } = render(<App />);
// find the input
const input = getByLabelText("search-input");
// search panel should not be rendered at this point
expect(queryByLabelText("search-panel")).not.toBeInTheDocument();
// this fire the onChange event and set the value to 'syd'
fireEvent.change(input, { target: { value: searchString } });
// useFetch should be called to get data
expect(axios.get).toHaveBeenCalled();
// assert our input value to be searchString
expect(input.value).toBe(searchString);
// search panel is loaded in the document
expect(queryByLabelText("search-panel")).toBeInTheDocument();
// wait for search results to be rendered
await wait(() => {
expect(queryByLabelText("search-results")).toBeInTheDocument();
});
});
我们使用async
是因为需要将await
搜索结果呈现出来。
就是这样。在 TypeScript 中模拟 Axios 的关键在于返回 AxiosResponse 的模拟文件。然后,我们可以在测试中使用 assert expect(axios.get).toHaveBeenCalled()
。
这是 github repo 的链接,因为 Codesandbox 不支持 jest.mock。
如果您尝试一下,您可以通过注释整个 axios.ts 文件和 App.test.tsx 文件中的 jest.mock('axios') 来了解它是如何工作的。
希望这对某人有帮助。
鏂囩珷鏉ユ簮锛�https://dev.to/mnsr/axios-mocking-in-react-using-typescript-4da0