前言
哈喽,大家好,我是海怪。
最近把项目里的 utils
以及 components
里的东西都测完了,算是完成了这次单测引入的第一个里程碑了。之后,我又把目光放到了 hooks
的文件夹上面,因为这些自定义 Hooks 一般都当工具包来使用,所以给它们上一上单测还是很有必要的。
正好我在 Kent C. Dodds 的博客里也发现了这篇 《How to test custom React hooks》,里面正好提到了如何高效地对自定义 Hooks 进行测试。今天就把这篇文章也分享给大家吧。
翻译中会尽量用更地道的语言,这也意味着会给原文加一层 Buf,想看原文的可点击 这里。
正片开始
如果你现在正在用 react@>=16.8
,那你可能已经在项目里写好几个自定义 Hooks 了。或许你会思考:如何才能让别人更安心地使用这些 Hooks 呢?当然这里的 Hooks 不是指那些你为了减少组件体积而抽离出来的业务逻辑 Hooks(这些应该通过组件测试来测的),而是那些你要发布到 NPM 或者 Github 上的,可重复使用的 Hooks。
假如现在我们有一个 useUndo
的 Hooks。
(这里 useUndo
的代码逻辑对本文不是很重要,不过如果你想知道它是怎么实现的,可以读一下 Homer Chen 写的源码)
import * as React from 'react'
const UNDO = 'UNDO'
const REDO = 'REDO'
const SET = 'SET'
const RESET = 'RESET'
function undoReducer(state, action) {
const {
past, present, future} = state
const {
type, newPresent} = action
switch (action.type) {
case UNDO: {
if (past.length === 0) return state
const previous = past[past.length - 1]
const newPast = past.slice(0, past.length - 1)
return {
past: newPast,
present: previous,
future: [present, ...future],
}
}
case REDO: {
if (future.length === 0) return state
const next = future[0]
const newFuture = future.slice(1)
return {
past: [...past, present],
present: next,
future: newFuture,
}
}
case SET: {
if (newPresent === present) return state
return {
past: [...past, present],
present: newPresent,
future: [],
}
}
case RESET: {
return {
past: [],
present: newPresent,
future: [],
}
}
default: {
throw new Error(`Unhandled action type: ${
type}`)
}
}
}
function useUndo(initialPresent) {
const [state, dispatch] = React.useReducer(undoReducer, {
past: [],
present: initialPresent,
future: [],
})
const canUndo = state.past.length !== 0
const canRedo = state.future.length !== 0
const undo = React.useCallback(() => dispatch({
type: UNDO}), [])
const redo = React.useCallback(() => dispatch({
type: REDO}), [])
const set = React.useCallback(
newPresent => dispatch({
type: SET, newPresent}),
[],
)
const reset = React.useCallback(
newPresent => dispatch({
type: RESET, newPresent}),
[],
)
return {
...state, set, reset, undo, redo, canUndo, canRedo}
}
export default useUndo
假如现在让我们来对这个 Hook 进行测试,提高代码可维护性。为了能最大化测试效果,我们应该确保我们的测试趋近于软件的真实使用方式。 要记住,软件的作用就是专门用来处理那些我们不想,或者不能手动去做的事的。写测试也是同理,所以先来想想我们会如何手动地测它,然后再来写自动化测试去替代手动。
我看到很多人都会犯的一个错就是:总是想 “Hook 嘛,不就是个纯函数么?就因为这样我们才喜欢用 Hook 的嘛。那是不是就可以像直接调普通函数那样,测试函数的返回值呢?” 对但是不完全对,它确实是个函数,但严格来说,它并不是 纯函数,你的 Hooks 应该是 幂等 的。如果是纯函数,那直接调用然后看看返回输出是否正确的就可以了。
然而,如果你直接在测试里调用 Hooks,你就会因为破坏 React 的规则,而得到这样的报错:
Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://fb.me/react-invalid-hook-call for tip