【送红宝书】JavaScript 测试系列实战(四):掌握 React Hooks 测试技巧
「为了回馈图雀社区的读者,图雀酱特地挑选了几本书籍送给大家,文末有送书活动详情哦~」
React Hooks 作为复用共同业务逻辑的强大工具,已经在开源库和业务代码中得到了广泛的使用。但是如果一个钩子没有完善的测试覆盖,我们就很难有信心去使用或者分享它。在这篇文章中,我们将体验强大的 react-hooks-testing-library,学习如何去测试钩子的同步和异步逻辑,并最终通过一个完整的例子去了解如何结合 Redux 框架进行测试。
开始使用 react-hooks-testing-library
在上一篇教程中,我们手工编写了非常原始的 React Hooks 测试代码。所幸,由于测试 React Hooks 的需求非常普遍,因此就有了测试 Hooks 的神器:react-hooks-testing-library。它提供了一系列专门用于测试 Hook 的工具函数,能够模拟在真实组件中使用 Hooks。
提示 如果你不熟悉 React Hooks 相关的知识,推荐先学习我们的 React Hooks 相关实战教程。
让我们先安装 react-hooks-testing-library:
npm install @testing-library/react-hooks
react-hooks-testing-library 中最重要的工具之一就是 renderHook
函数,它的工作方式与我们之前创建的 testHook
函数类似。它的参数是至少调用一个 Hook 的回调函数,返回值是一个对象,其中我们需要关心的是其中的 result
属性。result
属性又包含两个属性:
-
current
:所测试 Hook 的返回值 -
error
:所测试 Hook 抛出的错误(如果有的话)
让我们来结合实际的例子看一下。在之前 useModalManagement
钩子的测试代码中,我们仅仅只测试了调用 Hook 时不会报错。实际上,我们还希望测试以下用例:
- 默认渲染一个关闭的模态框
- 当调用
openModal
函数时,能够打开模态框
我们来看看新的测试代码:
// src/useModalManagement.test.js
import useModalManagement from './useModalManagement';
import { renderHook, act } from '@testing-library/react-hooks';
describe('The useModalManagement hook', () => {
it('should not throw an error', () => {
renderHook(() => useModalManagement());
});
it('should describe a closed modal by default', () => {
const { result } = renderHook(() => useModalManagement());
expect(result.current.isModalOpened).toBe(false);
});
describe('when the openModal function is called', () => {
it('should describe an opened modal', () => {
const { result } = renderHook(() => useModalManagement());
act(() => {
result.current.openModal();
});
expect(result.current.isModalOpened).toBe(true);
});
});
});
内容有点多,我们来逐个用例讲解:
-
测试 Hook 不会报错:我们将原来的
testHook
函数改成 react-hooks-testing-library 的renderHook
函数,这个函数接受的参数是一个调用 Hook 的函数 -
测试模态框默认关闭:还是通过
renderHook
渲染 Hook,然后获取到之前提到的result
对象,进一步通过result.current.isModalOpened
来获取到模态框的状态,然后用断言语句测试这个状态是false
(关闭状态) -
测试打开模态框:这个测试的难点在于怎么去触发
openModal
,所幸 react-hooks-testing-library 提供了act
工具函数来模拟浏览器中 Hook 的工作方式;act
函数同样接受一个函数执行一系列同步操作
注意 如果不使用
act
函数,而是直接将操作写在用例中,Jest 会抛出警告,并且可能会遇到一些棘手的边界情况。
通过 npm test
运行测试,全部通过!由于我们丰富了测试用例,对 useModalManagement
钩子的信心也大增!
测试异步钩子
刚才的 useModalManagement
涉及到的都是同步操作,然而在实际应用中,很多钩子都涉及到异步操作,例如 API 数据获取等。那么我们该怎么测试这些异步钩子呢?
实际上,刚才我们用到了 renderHook
的一个重要返回对象 result
,它实际上还提供了 waitForNextUpdate
函数。这个函数调用后会返回 Promise,这个 Promise 在下次渲染 Hook 时进入 Resolve 状态,非常适合用来测试异步更新的逻辑。
提示 react-hooks-testing-library 还提供了一些工具函数用来辅助异步钩子的测试,可参考官方文档的 Async Utilities 部分。
编写一个异步钩子
首先,让我们来写一个简单的异步钩子 useCommentsManagement
,用于从一个公共 API 获取一些评论数据,代码如下:
// src/useCommentsManagement.js
import { useState } from 'react';
function useCommentsManagement() {
const [comments, setComments] = useState([]);
function fetchComments() {
return fetch('https://jsonplaceholder.typicode.com/comments')
.then((response) => response.json())
.then((data) => {
setComments(data);
});
}
return {
comments,
fetchComments,
};
}
export default useCommentsManagement;
编写测试代码
然后我们来编写 useCommentsManagement
的测试代码如下:
// src/useCommentsManagement.test.js
import { renderHook, act } from '@testing-library/react-hooks';
import useCommentsManagement from './useCommentsManagement';
describe('The useCommentsManagement hook', () => {
describe('when the fetchComments function is called', () => {
it('should update the state after a successful request', async () => {
const { result, waitForNextUpdate } = renderHook(() => useCommentsManagement());
act(() => {
result.current.fetchComments();
});
await waitForNextUpdate();
return expect(result.current.comments.length).not.toBe(0);
});
});
});
在 act
函数中触发 fetchComments
拉取评论后,我们调用 waitForNextUpdate
并去 await
它返回的 Promise,当重渲染完成后,就可以使用调用断言语句来进行判断啦。这里我们还是通过 result.current
来获取评论数量。
注意 在编写 Jest 异步测试用例时,如果涉及到 Promise 的使用(包括
async/await
),要确保return
一个值,否则测试会超时。详细介绍请参考 Jest 异步测试文档。
继续 npm test
,一路绿灯!
提示 你也许还记得前面的课程中,我们讲到了如何用 Jest Mock 去避免发起真正的 HTTP 请求,从而能够保证测试不会因为网络问题而挂掉。至于怎么用 Mock 来写,就留给作业给你吧~
测试 Redux + Hooks
在规模较大的应用中,我们通常会使用一个状态管理库来解决复杂的数据流问题,而最受欢迎的选择无疑是 Redux。在这一节中,我们将手把手带你搭建一个完整的 Redux 模型,并且为之编写测试。
提示 这篇文章的重心不是 Redux,因此不会花太多的笔墨在这上面。如果不熟悉或者想复习一下的话,推荐阅读图雀社区的《Redux 包教包会》系列教程。
让我们先安装一下相关的依赖:
npm install redux react-redux
三件套:Action、Reducer 和 Store
之前的模态框钩子 useModalManagement
在内部维护了 isOpened
状态,这里我们将这个状态放到 Redux 中来进行管理。
首先定义相关的 Actions,创建 src/actions/modal.js
,代码如下:
// src/actions/modal.js
const OPEN_MODAL = 'OPEN_MODAL';
const CLOSE_MODAL = 'CLOSE_MODAL';
function openModal() {
return {
type: OPEN_MODAL,
};
}
function closeModal() {
return {
type: CLOSE_MODAL,
};
}
export { OPEN_MODAL, CLOSE_MODAL, openModal, closeModal };
然后是相关的 Reducer,代码如下:
// src/reducers/modal.js
import { OPEN_MODAL, CLOSE_MODAL } from '../actions/modal';
const initialState = {
isOpened: false,
};
export default function modal(state = initialState, action) {
if (action.type == OPEN_MODAL) {
return { isOpened: true };
} else if (action.type == CLOSE_MODAL) {
return { isOpened: false };
} else {
return state;
}
}
我们通过 combineReducers
将所有的 Reducer 结合成 rootReducer
(虽然这里只有一个 Reducer,但是这里为了完整地演示):
// src/reducers/index.js
import { combineReducers } from 'redux';
import modal from './modal';
const rootReducer = combineReducers({ modal });
export default rootReducer;
最后则是 Store,代码如下:
// src/store.js
import { createStore } from 'redux';
import rootReducer from './reducers';
const store = createStore(rootReducer);
export default store;
用 Redux 重写 useModalManagement
由于接入了 Redux,我们对之前的 useCommentsManagement
要来进行一波大刀阔斧的修改。修改后的代码如下:
// src/useModalManagement.js
import { useDispatch, useSelector } from 'react-redux';
import * as modalActions from './actions/modal';
function useModalManagement() {
const isModalOpened = useSelector((state) => state.modal.isOpened);
const dispatch = useDispatch();
function openModal() {
dispatch(modalActions.openModal());
}
function closeModal() {
dispatch(modalActions.closeModal());
}
return {
isModalOpened,
openModal,
closeModal,
};
}
export default useModalManagement;
这里我们使用 react-redux 提供的 useSelector
和 useDispatch
钩子来分别获取状态和派发函数。
OK,让我们把测试跑起来……居然报错了,主要的提示信息如下:
Invariant Violation: could not find react-redux context value; please ensure the component is wrapped in a <Provider>
含义很明确,我们没有提供 Redux 上下文。如果你熟悉 Redux 的话,你应该记得 react-redux 提供了 Provider
组件来向所有子组件提供 Store 对象,但是在测试的时候,我们该怎么让 Provider
去包裹待测试的钩子呢?
通过 wrapper 来提供上下文
幸运的是,renderHook
支持传入第二个参数,用于调节钩子的渲染配置,其中一个我们需要的配置就是 wrapper
。这个 wrapper
专门用来提供 React Context,当然也适用于 Redux 的 Provider
。
修改 useCommentsManagement
的测试代码如下:
// src/useModalManagement.test.js
import React from 'react';
import useModalManagement from './useModalManagement';
import { renderHook, act } from '@testing-library/react-hooks';
import { Provider } from 'react-redux';
import store from './store';
describe('The useModalManagement hook', () => {
// ...
it('should describe a closed modal by default', () => {
const { result } = renderHook(() => useModalManagement(), {
wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
});
expect(result.current.isModalOpened).toBe(false);
});
describe('when the openModal function is called', () => {
it('should describe an opened modal', () => {
const { result } = renderHook(() => useModalManagement(), {
wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
});
act(() => {
result.current.openModal();
});
expect(result.current.isModalOpened).toBe(true);
});
});
});
再次运行测试,又全部通过了!
小结
在这篇文章中,我们体验了强大的 react-hooks-testing-library,先后测试了同步和异步的钩子,最后还结合 Redux 来测了一波。在下一篇教程中,我们终于要接触激动人心的端到端(E2E)测试了,敬请期待吧!
- 【学术】一文搞懂自编码器及其用途(含代码示例)
- PhalApi-Zip--压缩文件处理类
- PhalApi-Xhprof -- Facebook开源的轻量级PHP性能分析工具
- OpenAI发布8个模拟机器人环境以及一种HER实现,以训练实体机器人模型
- PhalApi-APK--APK文件解包处理
- [喵咪PHP]页面显示空白问题
- 数据库中间件 Sharding-JDBC 源码分析 —— 结果归并
- PhalGo-Request
- PhalApi-Excel
- PhalGo-Viper获取配置
- Dubbo 源码解析 —— 集群容错架构设计
- PhalGo-ADM思想
- 数据库中间件 Sharding-JDBC 源码分析 —— JDBC实现与读写分离
- Pytorch 0.3.0 发布:新增张量函数,支持模型移植
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 简易数据分析(五):Web Scraper 翻页、自动控制抓取数量 & 父子选择器
- 【深度】韦东山:一文看看尽linux对中断处理的前世今生
- 嵌入式开发之交叉编译程序万能命令_以freetype为例
- Python-EEG处理和事件相关电位(ERP)
- 嵌入式Linux开发 配置网络
- 问号脸:为什么 Java 中 “1000==1000” 为 false,而 ”100==100“ 为 true?
- 【硬核】韦东山:使用freetype显示一行文字
- 动画函数封装
- 事件基础及操作元素
- JQuery生成图片列表
- Linux系统编程-几个多线程DEMO
- 自定义属性操作
- 节点操作
- 流程控制
- 【文末送书】Typescript 使用日志