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)测试了,敬请期待吧!
送红宝书
人的一生中总要读几本经典书,在这个“经典”泛滥的年代,什么才是权威的代表,我想大概是一本的书的口碑,能积累下上佳口碑的书,往往也是能经得住时间推敲的。比如这本:
在此基础上,接下来的各章揭示了 JavaScript 的基本概念,包括类、期约、迭代器、代理,等等。另外,书中深入探讨了客户端检测、事件、动画、表单、错误处理及 JSON。本书同时也介绍了近几年来涌现的重要新规范,包括 Fetch API、模块、工作者线程、服务线程以及大量新 API。
福利时间
关注公众号,在后台回复 “送书”,参与抽奖,有 2 本《JavaScript 高级程序设计》免费送哦~
PS:邀请好友参与抽奖可以提高中奖的概率,对这本书有兴趣的同学可以邀请好友参加哦~
开奖时间
2020-09-12 20:00
文章评论