🧑💻测试 React Hooks 的完整指南 🗓 + Demo 🍿
![](https://https://www.ooize.com/assets/uploads/2021/06/Snipaste_2021-06-25_14-20-57-1-1.png)
钩子阵营16.8介绍了2018年后期他们是函数勾成一个功能组件,并允许我们使用状态和组件的功能,如componentDidUpdate
,componentDidMount
等。这在以前是不可能的。
此外,钩子允许我们跨不同组件重用组件和状态逻辑。这在以前很难做到。因此,钩子已经改变了游戏规则。
在本文中,我们将探索如何测试 React Hooks。我们将选择一个足够复杂的钩子并进行测试。
我们希望您是一名已经熟悉 React Hooks的狂热 React 开发人员。如果你想复习知识,你应该查看我们的教程,这里是官方文档的链接。
我们将用于测试的钩子
对于本文,我们将使用我在上一篇文章Stale-while-revalidate Data Fetching with React Hooks 中编写的钩子。钩子被称为useStaleRefresh
。如果您还没有阅读这篇文章,请不要担心,因为我将在此处回顾该部分。
这是我们将要测试的钩子:
import { useState, useEffect } from "react";
const CACHE = {};
export default function useStaleRefresh(url, defaultValue = []) {
const [data, setData] = useState(defaultValue);
const [isLoading, setLoading] = useState(true);
useEffect(() => {
// cacheID is how a cache is identified against a unique request
const cacheID = url;
// look in cache and set response if present
if (CACHE[cacheID] !== undefined) {
setData(CACHE[cacheID]);
setLoading(false);
} else {
// else make sure loading set to true
setLoading(true);
setData(defaultValue);
}
// fetch new data
fetch(url)
.then((res) => res.json())
.then((newData) => {
CACHE[cacheID] = newData;
setData(newData);
setLoading(false);
});
}, [url, defaultValue]);
return [data, isLoading];
}
如您所见,useStaleRefresh
是一个钩子,它有助于从 URL 获取数据,同时返回数据的缓存版本(如果存在)。它使用一个简单的内存存储来保存缓存。
如果还isLoading
没有可用的数据或缓存,它还会返回一个值为 true的值。客户端可以使用它来显示加载指示器。isLoading
当缓存或新鲜响应可用时,该值设置为 false。
在这一点上,我建议你花一些时间阅读上面的钩子,以全面了解它的作用。
在本文中,我们将看到如何测试这个钩子,首先不使用测试库(只有 React Test Utilities 和 Jest),然后使用react-hooks-testing-library。
不使用测试库,即只使用一个测试运行器Jest
,背后的动机是为了演示测试钩子是如何工作的。有了这些知识,您将能够调试在使用提供测试抽象的库时可能出现的任何问题。
定义测试用例
在我们开始测试这个钩子之前,让我们想出一个我们想要测试的计划。既然我们知道钩子应该做什么,这是我测试它的八步计划:
- 当使用 URL 挂载钩子时
url1
,isLoading
istrue
和 data isdefaultValue
。 - 在异步获取请求之后,钩子用数据更新
data1
并且isLoading
是false
。 - 当 URL 更改为 时
url2
,isLoading
再次变为 true 并且数据为defaultValue
。 - 在异步获取请求之后,钩子被更新为新数据
data2
。 - 然后,我们将 URL 改回
url1
. 由于数据data1
被缓存,数据立即被接收。isLoading
是假的。 - 在异步获取请求之后,当收到新的响应时,数据将更新为
data3
。 - 然后,我们将 URL 改回
url2
. 由于数据data2
被缓存,数据立即被接收。isLoading
是假的。 - 在异步获取请求之后,当收到新的响应时,数据将更新为
data4
。
上面提到的测试流程清楚地定义了钩子将如何工作的轨迹。因此,如果我们能够确保此测试有效,我们就很好。
在没有库的情况下测试 Hook
在本节中,我们将看到如何在不使用任何库的情况下测试钩子。这将使我们深入了解如何测试 React Hooks。
为了开始这个测试,首先,我们想模拟fetch
. 这样我们就可以控制 API 返回的内容。这是嘲笑的fetch
.
function fetchMock(url, suffix = "") {
return new Promise((resolve) =>
setTimeout(() => {
resolve({
json: () =>
Promise.resolve({
data: url + suffix,
}),
});
}, 200 + Math.random() * 300)
);
}
此修改fetch
假定响应类型始终为 JSON,并且默认情况下返回参数url
作为data
值。它还为响应添加了 200 毫秒到 500 毫秒之间的随机延迟。
如果我们想改变响应,我们只需将第二个参数设置suffix
为非空字符串值。
说到这里,你可能会问,为什么要延迟?为什么我们不立即返回响应?这是因为我们想尽可能地复制现实世界。如果我们立即返回它,我们将无法正确测试钩子。当然,我们可以将延迟减少到 50-100 毫秒以加快测试速度,但在本文中我们不必担心。
准备好 fetch mock 后,我们可以将其设置为fetch
函数。我们使用beforeAll
andafterAll
这样做是因为这个函数是无状态的,所以我们不需要在单独测试后重置它。
// runs before any tests start running
beforeAll(() => {
jest.spyOn(global, "fetch").mockImplementation(fetchMock);
});
// runs after all tests have finished
afterAll(() => {
global.fetch.mockClear();
});
然后,我们需要在组件中安装钩子。为什么?因为钩子本身就是函数。在组件,它们可应对只有当useState
,useEffect
等等。
因此,我们需要创建一个TestComponent
帮助我们挂载钩子的工具。
// defaultValue is a global variable to avoid changing the object pointer on re-render
// we can also deep compare `defaultValue` inside the hook's useEffect
const defaultValue = { data: "" };
function TestComponent({ url }) {
const [data, isLoading] = useStaleRefresh(url, defaultValue);
if (isLoading) {
return <div>loading</div>;
}
return <div>{data.data}</div>;
}
这是一个简单的组件,它要么呈现数据,要么在数据正在加载(正在获取)时呈现“正在加载”文本提示。
一旦我们有了测试组件,我们就需要将它挂载到 DOM 上。我们使用beforeEach
和afterEach
为每个测试挂载和卸载我们的组件,因为我们想在每次测试之前从一个新的 DOM 开始。
let container = null;
beforeEach(() => {
// set up a DOM element as a render target
container = document.createElement("div");
document.body.appendChild(container);
});
afterEach(() => {
// cleanup on exiting
unmountComponentAtNode(container);
container.remove();
container = null;
});
请注意,container
它必须是一个全局变量,因为我们希望能够访问它以进行测试断言。
有了这个集合,让我们在渲染 URL 的地方做我们的第一个测试url1
,因为获取 URL 需要一些时间(参见 参考资料fetchMock
),它最初应该渲染“加载”文本。
it("useStaleRefresh hook runs correctly", () => {
act(() => {
render(<TestComponent url="url1" />, container);
});
expect(container.textContent).toBe("loading");
})
使用 运行测试yarn test
,它按预期工作。这是GitHub 上的完整代码。
现在,让我们测试此文loading
本何时更改为获取的响应数据url1
。
我们怎么做?如果您查看fetchMock
,您会看到我们等待了 200-500 毫秒。如果我们sleep
在等待 500 毫秒的测试中放入一个会怎样?它将涵盖所有可能的等待时间。让我们试试看。
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
it("useStaleRefresh hook runs correctly", async () => {
act(() => {
render(<TestComponent url="url1" />, container);
});
expect(container.textContent).toBe("loading");
await sleep(500);
expect(container.textContent).toBe("url1");
});
测试通过了,但我们也看到了一个错误(代码)。
PASS src/useStaleRefresh.test.js
✓ useStaleRefresh hook runs correctly (519ms)
console.error node_modules/react-dom/cjs/react-dom.development.js:88
Warning: An update to TestComponent inside a test was not wrapped in act(...).
这是因为useStaleRefresh
hook 中的状态更新发生在act()之外。为了确保及时处理 DOM 更新,React 建议您act()
在每次重新渲染或 UI 更新可能发生时使用。所以,我们需要结束我们的睡眠,act
因为这是状态更新发生的时间。这样做后,错误就会消失。
import { act } from "react-dom/test-utils";
// ...
await act(() => sleep(500));
现在,再次运行它(GitHub 上的代码)。正如预期的那样,它通过而没有错误。
我们来测试下一种情况,我们先将 URL 更改为url2
,然后检查loading
屏幕,然后等待 fetch 响应,最后检查url2
文本。由于我们现在知道如何正确等待异步更改,这应该很容易。
act(() => {
render(<TestComponent url="url2" />, container);
});
expect(container.textContent).toContain("loading");
await act(() => sleep(500));
expect(container.textContent).toBe("url2");
运行这个测试,它也通过了。现在,我们还可以测试响应数据发生变化和缓存发挥作用的情况。
您会注意到suffix
我们的fetchMock函数中有一个额外的参数。这是用于更改响应数据。所以我们更新我们的 fetch mock 以使用suffix
.
global.fetch.mockImplementation((url) => fetchMock(url, "__"));
现在,我们可以测试url1
再次设置 URL 的情况。它首先加载url1
然后url1__
. 我们可以对 做同样的事情url2
,应该不会有什么意外。
it("useStaleRefresh hook runs correctly", async () => {
// ...
// new response
global.fetch.mockImplementation((url) => fetchMock(url, "__"));
// set url to url1 again
act(() => {
render(<TestComponent url="url1" />, container);
});
expect(container.textContent).toBe("url1");
await act(() => sleep(500));
expect(container.textContent).toBe("url1__");
// set url to url2 again
act(() => {
render(<TestComponent url="url2" />, container);
});
expect(container.textContent).toBe("url2");
await act(() => sleep(500));
expect(container.textContent).toBe("url2__");
});
整个测试让我们确信钩子确实按预期工作(代码)。欢呼!现在,让我们快速浏览一下使用辅助方法的优化。
文章链接:https://www.ooize.com/%f0%9f%8d%bf%f0%9f%a7%91%f0%9f%92%bb-complete-guide-to-test-react-hooks-%f0%9f%97%93-demo-%f0%9f%8d%bf.html