原文地址:https://www.toptal.com/react/testing-react-hooks-tutorial

2018年底,React在16.8版本中引入了Hooks。它们(译注:指React Hooks)是连接函数组件并允许我们使用state和组件特性(如componentDidUpdatecomponentDidMount等)的函数。这在以前是不可能的。

同样地,hook允许我们在不同的组件之间重用组件和状态逻辑,这在以前很难做到的。 因此,hook已经改变完全了游戏规则。

在本文中,我们将探讨如何测试React Hooks,我们会选择开发一个复杂的hook并对其进行测试。

我们希望你是一个已经熟悉React Hooks的React开发人员。如果你想了解更多的知识,则应该阅读我们的教程,这是官方文档的链接(PS. 链接与本文无关,不贴了)。

用于测试的hook

本文我们将使用我在上一篇文章中编写的hook,即Stale-while-revalidate Data Fetching with React Hooks。这个hook称为useStaleRefresh。如果你还没有读过这篇文章,不用担心,我将在这里重述这一部分。

这就是我们将要测试的hook:

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 requestconst cacheID = url;// look in cache and set response if presentif (CACHE[cacheID] !== undefined) {setData(CACHE[cacheID]);setLoading(false);} else {// else make sure loading set to truesetLoading(true);setData(defaultValue);}// fetch new datafetch(url).then((res) => res.json()).then((newData) => {CACHE[cacheID] = newData;setData(newData);setLoading(false);});}, [url, defaultValue]);return [data, isLoading];
}

可见,useStaleRefresh是一个hook,它从URL获取数据,同时返回数据的缓存版本(如果存在的话),它使用一个简单的内存存储来保存缓存。

如果尚无可用的的数据或缓存,它还返回一个值为trueisLoading状态,客户端可以使用它来展示加载中标识。当缓存或新响应可用时,isLoading值设置为false

PS. 我建议花一些时间阅读上面的hook源码,以全面了解它的作用。

在本文中,我们将看到如何测试该hook,首先不使用任何测试库(仅使用React Test Utilities和Jest),然后使用react-hooks-testing-library。

不使用测试库的目的是为了演示如何测试hook。 有了这些知识,你将能够debug在使用提供测试抽象的库(译注:例如上面提到的react-hooks-testing-library,就属于测试库)时可能出现的任何问题。

定义测试用例

在开始测试此hook之前,需要先准备测试计划,明确需要测试什么。既然我们知道hook应该做什么,这是我测试它的8步计划:

  1. 当hook使用URL url1挂载时,isLoadingtrue,数据为defaultValue
  2. 在异步获取请求之后,该hook将使用数据data1更新(组件),并且isLoadingfalse
  3. 当URL更改为url2时,isLoading再次变为true,数据为defaultValue
  4. 在异步获取请求之后,该hook将使用新数据data2更新(组件)。
  5. 然后,我们将URL改回url1。由于已缓存数据data1,因此会立即使用它。isLoading被设置为false
  6. 在异步获取请求之后,当接收到新的响应时,数据将更新为data3
  7. 然后,我们将URL改回url2。由于已缓存数据data2,因此会立即使用它。isLoading被设置为false
  8. 在异步获取请求之后,当接收到新的响应时,数据将更新为data4

上面提到的测试流程清楚地定义了hook是如何运行的。因此,如果我们可以确保此测试有效,那么说明hook是正确的。

不依赖测试库测试

在本节中,我们将了解如何在不使用任何测试库的情况下测试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。此外还为响应增加了200ms至500ms之间的随机延迟。

如果要更改响应,只需将第二个参数suffix设置为非空字符串值。

此时,你可能会问,为什么要延迟?我们为什么不立即返回响应?这是因为我们想尽可能地模拟现实世界。如果我们立即返回hook,我们将无法正确测试hook。 当然,我们可以将延迟降低到50-100毫秒,以便进行更快的测试,但是在本文中我们不需要担心这个问题。

准备好fetch模拟后,可以将其赋值给fetch函数。 我们使用beforeAllafterAll来实现是因为该功能是无状态的,因此我们无需在每次测试后将其重置。

// runs before any tests start running
beforeAll(() => {jest.spyOn(global, "fetch").mockImplementation(fetchMock);
});// runs after all tests have finished
afterAll(() => {global.fetch.mockClear();
});

然后,我们需要将hook挂载到组件中。为什么呢?因为hooks只是它们(译注:指组件)的一种能力,只有在组件中使用时,hooks才能响应useStateuseEffect等。

因此,我们需要创建一个TestComponent以帮助我们挂载hook。

// defaultValue是一个全局变量,用来避免在重新渲染时改变对象指针
// 我们还可以深入比较hook的useEffect内部的'defaultValue'
const defaultValue = { data: "" };function TestComponent({ url }) {const [data, isLoading] = useStaleRefresh(url, defaultValue);if (isLoading) {return <div>loading</div>;}return <div>{data.data}</div>;
}

这是一个简单的组件,它要么render数据,要么render一个“loading”文本提示。

有了测试组件后,需要将其挂载到DOM中。对于每一次测试,我们使用beforeEachafterEach来挂载和卸载组件,因为我们希望在每次测试之前都是一个全新的DOM结构。

let container = null;beforeEach(() => {// 指定一个DOM元素作为render targetcontainer = document.createElement("div");document.body.appendChild(container);
});afterEach(() => {// cleanup on exitingunmountComponentAtNode(container);container.remove();container = null;
});

注意,container必须是一个全局变量,因为我们需要访问它来进行测试断言。

设置完成后,让我们进行第一个测试。我们在其中渲染URL url1,并且由于获取URL会花费一些时间(请参阅fetchMock),因此它应该首先渲染“loading”文本。

it("useStaleRefresh hook runs correctly", () => {act(() => {render(<TestComponent url="url1" />, container);});expect(container.textContent).toBe("loading");
})

运行yarn test命令执行测试,其工作正常。完整的测试代码请参阅GitHub。

现在,让我们测试一下这个“loading”文本何时更改为获取到的响应数据,url1

我们怎么做呢?从fetchMock得知,fetch首先会等待200-500毫秒。如果我们在测试中延迟500毫秒会怎么样?它将覆盖fetch所有可能的等待时间。让我们试试。

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:88Warning: An update to TestComponent inside a test was not wrapped in act(...).

这是因为useStaleRefresh hook中的状态更新发生在act()之外。为了确保DOM更新得到及时处理,React建议在每次可能发生重新呈现或UI更新时使用act()包裹。因此,我们需要用act包装sleep,因为这里是状态更新发生的时机。这样之后,错误就消失了:

import { act } from "react-dom/test-utils";
// ...
await act(() => sleep(500));

现在,再次运行它(GitHub源码)。正如预期的那样,没有再报错了。

让我们测试下一种情况,首先将URL更改为url2,然后检查loading界面,然后等待响应返回,最后检查url2文本。既然我们现在知道如何正确地等待异步更改,这应该很容易(写测试用例)。

act(() => {render(<TestComponent url="url2" />, container);
});
expect(container.textContent).toContain("loading");await act(() => sleep(500));
expect(container.textContent).toBe("url2");

运行这个测试用例,它也通过了。现在,我们还可以测试响应数据变化或缓存起作用的情况。

请注意到在fetchMock函数中有一个额外的参数suffix,这是用于修改响应数据的。因此,我们通过使用suffix参数修改响应数据。

global.fetch.mockImplementation((url) => fetchMock(url, "__"));

现在,我们可以再次测试URL设置为url1的情况。它首先加载url1,然后加载url1__。我们也可以对url2执行同样的操作,结果将是一样的。

it("useStaleRefresh hook runs correctly", async () => {// ...// new responseglobal.fetch.mockImplementation((url) => fetchMock(url, "__"));// set url to url1 againact(() => {render(<TestComponent url="url1" />, container);});expect(container.textContent).toBe("url1");await act(() => sleep(500));expect(container.textContent).toBe("url1__");// set url to url2 againact(() => {render(<TestComponent url="url2" />, container);});expect(container.textContent).toBe("url2");await act(() => sleep(500));expect(container.textContent).toBe("url2__");
});

整个测试下来让我们确信hook确实按照预期工作(源码)。nice!现在,让我们快速了解一下使用辅助方法对测试过程进行优化。

使用辅助方法优化测试

到目前为止,我们已经了解了如何完整地测试我们的hook。这种方法是有效的,但其实并不完美。所以,我们能做得更好吗?

答案是肯定的!可以看到,每一次测试我们都需要等待固定的500ms以确保fetch完成,而每个请求会花费200到500ms不等的时间。所以,这显然是在浪费时间。我们可以通过等待每个请求所花费的实际时间来更好地处理这个问题。

那怎么做到呢?一种简单的方案是是(一直)执行断言判断,直到它执行到或者超时。这里创建一个waitFor函数来完成这个任务:

async function waitFor(cb, timeout = 500) {const step = 10;let timeSpent = 0;let timedOut = false;while (true) {try {await sleep(step);timeSpent += step;cb();break;} catch {}if (timeSpent >= timeout) {timedOut = true;break;}}if (timedOut) {throw new Error("timeout");}
}

该函数只是简单地每10ms在try...catch块内执行一次回调(cb),如果超时,则会抛出错误。这使得我们可以一直执行断言直到以安全的方式通过(即没有无限循环)为止。

可以在测试中使用它,如下所示:我们不用等待500ms然后再执行断言,而是使用了waitFor函数。

// INSTEAD OF
await act(() => sleep(500));
expect(container.textContent).toBe("url1");
// WE DO
await act(() =>waitFor(() => {expect(container.textContent).toBe("url1");})
);

把所有的测试断言都按照这样修改之后,可以看到测试用例的运行速度会有显著的差异(源码)。

目前为止都很棒,但是有时我们不想通过UI来测试hook,而是想使用hook的返回值来测试,那么该怎么做呢?

这并不困难,因为我们已经可以访问hook的返回值了,只是它们在组件内部。如果能把这些变量放到全局可访问,它就可以做到只测试返回值了。

因为我们将通过hook的返回值而不是渲染的DOM来测试hook,所以可以从组件中删除HTML,并使其渲染null。此外还应该删除hook返回中的析构,使其更通用。以下是更新后的测试组件:

// global variable
let result;function TestComponent({ url }) {result = useStaleRefresh(url, defaultValue);return null;
}

现在,hook的返回值存储在result中,一个全局变量,可以访问它来获取断言结果。

// INSTEAD OF
expect(container.textContent).toContain("loading");
// WE DO
expect(result[1]).toBe(true);// INSTEAD OF
expect(container.textContent).toBe("url1");
// WE DO
expect(result[0].data).toBe("url1");

修改(所有的用例)之后,可以看到测试通过了(源码)。

至此,我们知道了测试React Hooks的要点。 但仍然可以进行一些改进,例如:

  1. result变量移动到局部变量
  2. 没必要每测试一个hook时都新建一个测试组件

可以通过创建一个自带测试组件的工厂函数来实现。它还应该在测试组件中执行要测试的hook,并允许外部访问result变量,下面来看看怎么做。

首先,将TestComponentresult移动到函数内。然后还需要将hook和hook参数作为函数的参数传递进去,以便它们可以在测试组件中使用它。这里将这个函数命名为renderHook

function renderHook(hook, args) {let result = {};function TestComponent({ hookArgs }) {result.current = hook(...hookArgs);return null;}act(() => {render(<TestComponent hookArgs={args} />, container);});return result;
}

之所以将hook的返回值存储在result.current中,是因为我们希望在测试运行时(能够)更新返回值。 此处(指useStaleRefresh)hook的返回值是一个数组,如果直接返回,那么(renderHook)将返回hook的值拷贝(导致无法再次更新)。通过将其存储在对象中并返回对该对象的引用,可以通过更新result.current来更新(renderHook的)返回值。

现在,如何去更新hook呢?由于已经使用了一个闭包,因此再封装另一个函数来实现此功能。最终的renderHook函数如下所示:

function renderHook(hook, args) {let result = {};function TestComponent({ hookArgs }) {result.current = hook(...hookArgs);return null;}function rerender(args) {act(() => {render(<TestComponent hookArgs={args} />, container);});}rerender(args);return { result, rerender };
}

现在,我们可以在测试用例中使用它,而无需再使用act和render:

const { rerender, result } = renderHook(useStaleRefresh, ["url1",defaultValue,
]);

然后,可以使用result.current进行断言,并使用rerender函数更新hook。 以下是一个简单的示例:

rerender(["url2", defaultValue]);
expect(result.current[1]).toBe(true); // check isLoading is true

修改(所有的用例)之后,你将看到它们可以测试通过了,没有任何问题(源码)。

nice!现在我们有了更加清晰的抽象来测试hook了。但仍然可以有改进的地方,例如,即使保持不变,也需要每次rerender都传递defaultValue

但是,不要太过在意,因为我们已经有了一个可以大大改善这种体验的测试库,它就是react-hooks-testing-library。

使用React-hooks-testing-library

React-hooks-testing-library可以完成我们之前讨论的所有内容甚至更多。比如,它会处理container的挂载和卸载,因此你不必在测试文件中重复这些操作,这使得我们集中精力测试hook。

它带有一个renderHook函数,返回rerender方法和result对象。此外它还会返回与waitFor类似的wait方法,你不必自己去实现。

下面是我们在React-hooks-testing-library库中render hook的方法。注意,hook是以callback的形式传入的,每次测试组件re-render时callback都会重新执行。

const { result, wait, rerender } = renderHook(({ url }) => useStaleRefresh(url, defaultValue),{initialProps: {url: "url1",},}
);

现在,我们可以测试第一次render是否导致isLoadingtrue并返回值为defaultValue。这与上面实现的完全相似。

expect(result.current[0]).toEqual(defaultValue);
expect(result.current[1]).toBe(true);

为了测试异步更新,可以使用renderHook返回的wait方法。因为它内部已经封装了act(),所以不需要在它包裹act()了。

await wait(() => {expect(result.current[0].data).toEqual("url1");
});
expect(result.current[1]).toBe(false);

然后,可以使用rerender来用新props更新hook返回值了。请注意,此处无需再传defaultValue

rerender({ url: "url2" });

最后,剩下的测试用例也可以相对简单地进行(源码)。

结语

我的目的是通过一个简单的async hook来向你展示如何测试React Hooks。我希望这可以帮助你自信地解决任何类型的hooks的测试,因为文章介绍的方法应该可以适用于大部分hooks测试。

我建议使用React-hooks-testing-library,因为它已经足够完善了,到目前为止,我还没有遇到任何重大的问题。 如果确实遇到问题,你现在应该已经知道如何使用本文所述的方法来解决了。

mounty不可重新挂载因为先前没有完全卸载_【译】React Hooks测试完全指南相关推荐

  1. 取消挂载点可以节省磁盘么_磁盘克隆、磁盘镜像还有复制粘贴有什么不一样?...

    最近在倒腾新SSD和用了三四年的老操作系统,期间在磁盘上创建个新的分区,并且安装了一个全新的Windows10,结果,一不小心覆盖了老系统的引导,系统丢了.然后,用各种引导工具进行了修复,好在几次有惊 ...

  2. ubuntu本机 使用 sshfs 临时挂载 远程服务器 硬盘 与 卸载

    1. 先安装 sshfs sudo apt install sshfs 2. 挂载 更改文件/etc/fuse.conf,将#user_allow_other 的#删除, 主要避免挂载后文件权限问题 ...

  3. 存储设备映射Linux服务器,青云oss对象存储映射至linux服务器

    参考资料: ? ? 源码安装 1. 编译要求 GCC 4.1.2 或更高版本 CMake?3.0 或更高版本 ? 2. 安装依赖 sudo yum install fuse fuse-devel li ...

  4. Mac下使用Mounty挂载NTFS出现了文件不能拷贝的解决办法

    Mac下使用Mounty挂载NTFS出现了文件不能拷贝的解决办法 cd 文件所在目录,输入命令 xattr -d com.apple.FinderInfo * 扩展知识分隔线: ----------- ...

  5. 已解决-Mounty 挂载NTFS报错:卷“BOOTCAMP“不可重新挂载

    问题: 我在macbook air上安装了win10双系统,安装双系统完成后,Windows磁盘在macOS下不可以访问,已经安装的Mounty软件挂载Windows磁盘报错: 卷"BOOT ...

  6. 开机自动挂载与autofs触发挂载

    开机自动挂载与autofs触发挂载 实验背景:在Linux服务器中,格式化好的文件系统要有一个"挂载"的过程,然后才能通过挂载点文件夹访问该文件系统.那如何挂载各种不同类型的文件系 ...

  7. CentOS6.5挂载windows共享文件夹

    由于工作需要,需要把本机的文件夹共享出去,然后让CentOS服务器临时使用下. 服务器使用的是CentOS系统,而本机使用的win7系统.考虑到是临时使用,所以就不打算搭建FTP和Samba服务器,直 ...

  8. linux(CentOS)磁盘挂载数据盘

    linux(CentOS)磁盘挂载数据盘: 第一步:查看是否存在需要挂载的磁盘: sudo fdisk -l 第二步:为需要挂载的磁盘创建分区: sudo fdisk /dev/vdb 执行中:依次选 ...

  9. linux umount swap,挂载、卸载、free查看内存情况、创建交换分区、回环设备、dd命令、自动挂载、fuser...

    挂载.卸载 分区.格式化创建了文件系统后就可以挂载了 挂载:将新的文件系统关联至当前根文件系统 卸载:将某文件系统与当前根文件系统的关联关系移除 mount挂载 使用方法: mount 设备 挂载点 ...

最新文章

  1. python读取指定行的txt_【Python】读取txt文件,获取指定行中指定位置数据
  2. c++多字节与宽字节字符串转换(windows平台)
  3. flashisland in webdynpro
  4. java类加载-ClassLoader双亲委派机制
  5. spring整合mybatis采坑
  6. 以DES的方式实现对称加密,并提供密钥
  7. 信息学奥赛一本通(1221:分成互质组)
  8. centos-安装ifconfig
  9. 【Java 进阶】匿名类(代码传递、回调、过滤器)、Lambda表达式(方法引用)、函数式接口(Supplier、Consumer、Predicate、Function)
  10. python基础教程第三版-Python基础教程(第三版)(七)再谈抽象
  11. OpenCR介绍以及自制OpenCR
  12. dubbo超时机制原理
  13. qq空间显示手机型号android,qq说说显示手机型号 qq说说显示手机型号在哪里设置...
  14. Linux周立功CAN驱动安装指导
  15. 计算机论文刊物发表,计算机论文发表流程
  16. 金仓数据库KingbaseES的连接方法
  17. (十六)Hibernate中的延迟加载
  18. 访客模式 无痕模式 区别_旧访客设计模式的新生活
  19. 【论文阅读】Rethinking Spatiotemporal Feature Learning For Video Understanding
  20. ENVI将高程数据拼接并转换为.dem或.dat_bil格式——以GDEM数据为例

热门文章

  1. 050_Unicode字符官方标准一
  2. 06_一对一和一对多
  3. 011_JavaScript数据类型
  4. 性能测试服务器数量与线上数量不同,性能需求分析
  5. redis stream持久化_Beetlex.Redis之Stream功能详解
  6. react 动态添加组件属性_这么高质量React面试题(含答案),看到就是赚到了!...
  7. Wireshark网络抓包实践
  8. windows上部署nginx---nginx启动
  9. c# redis hashid如何设置过期时间_Redis数据库实现原理(划重点)
  10. ddl是什么意思网络语_DDL语句是啥