在 React 为什么须要 Hook 中咱们探讨了 React 为什么须要引入 Hook 这个属性,在 React Hook 实战指南中咱们深刻理解了各种 Hook 的具体用法以及会遇到的问题,在本篇文章中我将带大家理解一下如何通过为自定义 hook 编写单元测试来进步咱们的代码品质,它会蕴含上面的内容:
-
什么是单元测试
- 单元测试的定义
- 为什么须要编写单元测试
- 单元测试须要留神什么
-
如何对自定义 Hook 进行单元测试
- Jest
- React-hooks-testing-library
- 例子
什么是单元测试
单元测试的定义
要了解单元测试,咱们先来给测试下个定义。用最简略的话来说测试就是:咱们给被测试对象一些输出(input),而后看看这个对象的输入后果(output)是不是合乎咱们的预期(match with expected result)。而在软件工程外面有很多不同类型的测试,例如单元测试(unit test),功能测试(functional test),性能测试(performance test)和集成测试(integration test)等。不同品种的测试的次要区别是被测试的对象和评判指标不一样。对于单元测试,被测试的对象是咱们源代码的 独立单元
(individual unit),在面向过程编程语言(procedural programming)外面,单元就是咱们封装的办法(function),在面向对象的编程语言(object-oriented programming)外面单元是类(class)的办法(method),咱们个别不举荐将某个类或者某个模块间接作为单元测试的单元,因为这会使被测试的逻辑过于宏大,而且问题呈现时不容易进行定位。
为什么须要编写单元测试
理解了单元测试的定义后,咱们再来探讨一下为什么咱们要在代码外面进行单元测试。
咱们之所以要在我的项目中编写单元测试,次要是因为对代码进行单元测试有上面这些益处:
进步代码品质
单元测试能够进步咱们的代码品质次要体现在它能够在咱们开发某个性能的时候 提前帮咱们发现自己编写的代码的 bug。举个例子,如果 A 同学写了一个叫做 useOptions
的 hook 它承受一个叫做 options
的参数,这个参数既能够是一个对象也能够是一个数组。A 同学本人开发的过程中他只试过给 useOptions
传对象而没有试过给它传数组。同一个我的项目的 B 同学在应用 useOptions
的时候给它传了个数组发现代码挂了,这个时候 B 同学就得找 A 同学确认并期待 A 同学修复这个问题,这岂但会影响 B 同学的开发进度而且还会让 B 同学感觉 A 同学 不靠谱
,或者感觉 A 同学的 代码很烂
。如果 A 同学有对useOptions
进行单元测试的话,这个 喜剧
可能就不会产生了,因为 A 同学在为 useOptions
编写单元测试的时候就思考了 options
为数组的状况,并且在 B 同学应用之前就修复了这个问题。因而编写单元测试能够让咱们在开发的过程中提前思考到很多前面应用才会发现的问题,进而进步咱们的代码品质。
不便代码重构和新性能增加
编写单元测试的过程其实是咱们给代码编写 应用说明书的过程
(specification)。这个 应用说明书
非常重要,它相当于 代码生产者
(producer)与 代码消费者
(consumer)之间的 合约
(contract),生产者须要保障 在消费者应用代码没错的前提下 代码要有 应用说明书
下面的成果。这其实会对代码生产者起到肯定的制约作用,因为生产者必须保障无论是给原来的代码增加新的性能还是对它进行重构,它都要满足原来 应用说明书
上的要求。
持续下面那个例子,A 同学和 B 同学都在我的项目的 1.0.0
版本中应用了 useOptions
这个 hook,尽管 useOptions
没有编写单元测试,可是代码是没有 bug 的(最起码没有被发现)。前面我的项目须要进行 2.0.0
版本的降级了,这时候 A 同学须要为 useOptions
增加新的性能,A 同学在改变了 useOptions
的代码后,在本人应用到的中央(对象作为参数的中央)做了测试,没有发现 bug。在 A 同学自测完代码后,并将这个更改集成(integration)到了我的项目的 master
分支上。前面 B 同学在更新完 A 同学的代码后,发现自己的代码呈现了一些问题,这个时候 B 同学很可能就会慌手慌脚,并且可能须要破费一段时间能力定位到原来是 A 同学对 useOptions
的改变影响到他的性能,这除了会影响到我的项目的进度外还会让 A 同学和 B 同学的关系进一步好转。这个喜剧同样也是能够通过编写单元测试来防止的,试想一下如果 A 同学有给 useOptions
编写配套的 应用说明书
(单元测试),A 同学在改变完代码后,它的代码是通过不了 应用说明书
的查看的,因为它的改变扭转了 useOptions
之前定义好的内部行为,这个时候 A 同学就会提前修复本人的代码进而防止了 B 同学前面的苦恼。通过这个例子大家可能还是没有领会到单元测试对于咱们平时产品迭代或者代码重构的重要性,可是你试想一下在一个比拟大的我的项目中是有很多个 A 同学和 B 同学的,也有成千上万个 useOptions
函数,当真的产生相似问题的时候 bug 将会更难被定位和修复,如果咱们大部分的代码都有单元测试的话,无论是对代码减少新的性能还是对原来的代码进行重构咱们都会更有信念。
欠缺咱们代码的设计
在软件工程外面有个概念叫做 测试驱动开发
(Test-driven Development),它激励咱们在 理论开始编码之前先为咱们的代码编写测试用例 。这样做的目标是让咱们在开发之前就以 代码使用者
的角度去评判咱们的代码设计。如果咱们的代码设计很蹩脚,咱们就会发现咱们很难为它们编写详尽的单元测试用例,相同如果咱们的代码设计得很好(低耦合高内聚),各个函数的参数和性能都设计得非常正当,咱们就非常容易就为它们编写对应的单元测试。咱们要记住一句话:高质量的代码肯定是能够被测试的(testable)。那么为什么是在还没开始写代码之前就编写测试用例呢?这是因为如果咱们在代码写完之后再编写测试的话,即便咱们发现代码设计得再不合理,咱们也没有能源去改了,因为对设计的改变可能会让咱们重写所有的代码,所以咱们须要在理论编码之前进行单元测试的编写,因为这个时候的 改代码阻力
是最小的。
提供文档性能
咱们在为代码编写单元测试的时候实际上是在为代码编写一个个 应用例子
,因而别的开发者在应用咱们代码的时候能够通过咱们的单元测试来疾速把握咱们定义的各种函数的用法。另外教大家一个实用的技巧:如果咱们发现某个库的文档不是很全面的话,能够通过查看这个库的单元测试来疾速把握这个库的用法。
单元测试须要留神的问题
隔离性
下面咱们说到单元测试是对代码独立的单元进行测试,这个独立的意思不是说这个函数(单元)不会调用另外一个函数(单元),而是说咱们在测试这个函数的时候如果它有调用到其它的函数咱们就须要 mock 它们,从而将咱们的测试逻辑 只放在被测试函数的逻辑上,不会受到其它依赖函数的影响。举个例子咱们当初要测试以下函数:
async function fetchUserDetails(userId) {const userDetail = await fetch(`https://myserver.com/users/${userId}`)
return userDetail
}
在测试 fetchUserDetails
时咱们就须要 mock fetch
这个函数了,因为咱们当初测试的函数是 fetchUserDetails
,咱们只须要确定在外界调用fetchUserDetails
的时候 fetch
会被调用,并且调用的参数是 “https://myserver.com/users/${userId}”
就行了,至于 fetch
函数如何发申请和解决返回来的数据都是 fetch
函数本人的事,咱们不应该在测试 fetchUserDetails
的时候关怀这个问题。
单元测试要留神隔离性的另外一个起因是它能够保障当测试案例失败的时候咱们能够非常容易定位到问题的所在。以下面的代码为例,如果咱们没有 mock fetch
函数,一旦咱们的测试失败,咱们很难分清是 fetchUserDetails
逻辑错了还是 fetch
的逻辑错了。
可重复性
咱们编写的所有单元测试用例肯定不能依赖内部的运行环境,否则咱们的单元测试将不具备 可重复性
(repeatable)。所谓的 可重复性
就是:如果咱们的单元测试用例当初是能够通过的,那么 在代码不产生变动和测试用例没有扭转的前提下 它将是始终能够通过的。举个测试用例不具备可重复性的例子,如果你将我的项目的单元测试数据全副放在数据库外面,你明天运行我的项目的测试用例是能够通过的,而第二天其他人无心改了数据库的数据,这个时候你的测试用例就通过不了了,咱们就说这些测试用例不具备可重复性,呈现这个问题的次要起因是 它们应用了内部的依赖作为测试条件
。由此可见要使咱们的测试用例具备可重复性的一个关键点是在编写单元测试的时候防止内部依赖,这些内部依赖包含 数据库
, 网络申请
和本地文件系统
等。
另外一个影响到测试用例可重复性的一个重要的却容易被疏忽的因素是:不同单元测试用例之间共用了一些测试数据,某个测试用例对测试数据的更改可能会影响其它测试用例的正确执行。因而咱们在编写单元测试用例的时候肯定要 防止不同测试用例之间共用一些测试数据 ,尽量将每个测试用例 隔离
起来。
进步代码覆盖率
在单元测试外面有个概念叫做代码覆盖率(test coverage),它表明咱们代码 被测试的水平
。举个例子如果咱们有一个 100 行的函数,在咱们运行完所有的为这个函数编写的单元测试用例之后,如果测试框架通知咱们这个函数的覆盖率是 80%,这表明咱们的测试用例代码只笼罩了这个函数的 80 行代码,还有一些代码分支(if/else, switch, while)没有被执行到。如果咱们想通过单元测试来进步咱们代码品质的话,咱们就须要保障咱们代码的覆盖率足够大,尽量让被测试的函数的每一种被执行状况都被笼罩到(覆盖率 100%),特地是一些异样的状况应该也要被笼罩到(例如参数谬误,调用第三方依赖报错等),这样咱们能力及早地发现代码的 bug 并进行修复。
测试用例运行工夫要短
我在下面说到单元测试是能够帮忙咱们更好地进行代码迭代和重构的,要做到这点其实要求咱们在每次代码归并的时候对被 merge
的代码进行一些自动化检测(CI),这就包含我的项目单元测试用例的运行。试想一下在一个比拟大型的我的项目外面单元测试用例的数量往往是很多的,少则几百个,多则上千个,如果全副运行所有测试用例的工夫须要十几分钟甚至一两小时,这就会影响到代码集成的进度。为了防止这个问题,咱们就须要确保每个单元测试用例执行的工夫不能过长,例如防止在测试代码外面进行一些耗时的计算等。
如何对自定义 Hook 进行单元测试
在 React Hook 实战指南中咱们提到 Hook 就是一些函数,所以对 Hook 进行单元测试其实是对一个函数进行测试,只不过这个函数和一般函数的区别是它领有 React 给它赋予的非凡性能。在讲如何对 Hook 进行测试之前咱们先来理解一下咱们要用到的测试框架 Jest 和 hook 测试库 react-hook-testing-library。
Jest
Jest 是 Facebook 开源的一个单元测试框架,它的使用率和知名度都十分高,一些驰名的开源我的项目例如 webpack, babel 和 react 等都是应用 Jest 来进行单元测试的,因为这篇文章的重点不是 Jest 的应用,所以我在这里将不为大家做具体的介绍,这里次要介绍一下咱们罕用到的 Jest API:
罕用 API
it/test
it/test
函数是用来定义 测试用例
(test case)的,它的函数签名是it(description, fn?, timeout?)
,description
参数是对这个测试用例的一个简短的形容,fn
是一个运行咱们理论测试逻辑的函数,而 timeout 则是这个测试用例的超时工夫。上面是一个简略的例子:
import sum from 'somewhere/sum'
it('test if sum work for positive numbers', () => {const result = sum(1, 2)
expect(result).toEqual(3)
})
describe
describe
函数是用来给测试用例 分组
用的,它的函数签名是 describe(description, fn)
,description 是用来形容这个分组的,而fn
函数外面则能够定义内嵌的分组(nested)或者是一些测试用例(it),上面是一个简略的例子:
import sum from 'somewhere/sum'
describe('test sum', () => {it('work for positive numbers', () => {const result = sum(1, 2)
expect(result).toEqual(3)
})
it('work for negative numbers', () => {const result = sum(-1, -2)
expect(result).toEqual(-3)
})
})
expect
咱们在刚开始的时候就提到所谓的测试就是要 比拟被测试对象的输入和咱们期待的输入是不是统一的 ,也就波及到一个比拟的过程,在 Jest 框架中咱们能够通过expect
函数来拜访一系列 matcher
来进行这个 比拟的过程
,例如下面的expect(sum).toEqual(3)
就是一个用 matcher 来判断输入后果是不是咱们想要的值的过程。对于更加具体的 matcher 信息大家能够参考 jest 的官网文档。
mock
在 Jest 框架中用来进行 mock 的办法有很多,次要用到的是 jest.fn()
和jest.spyOn()
。
jest.fn
jest.fn
会生成一个 mock 函数,这个函数能够用来代替源代码中被应用的第三方函数。jest.fn
生成的函数下面有很多属性,咱们也能够通过一些 matcher
来对这个函数的调用状况进行一些断言,上面是一个简略的例子:
// somewhere/functionWithCallback.js
export const functionWithCallback = (callback) => {callback(1, 2, 3)
}
// somewhere/functionWithCallback.spec.js
import {functionWithCallback} from 'somewhere/functionWithCallback'
describe('Test functionWithCallback', () => {it('if callback is invoked', () => {const callback = jest.fn()
functionWithCallback(callback)
expect(callback.mock.calls.length).toEqual(1)
})
})
jest.spyOn
咱们源代码中的函数可能应用了另外一个文件或者 node_modules
中装置的一些依赖,这些依赖能够应用 jest.spyOn
来进行 mock,上面是一个简略的例子:
// somewhere/sum.js
import {validateNumber} from 'somewhere/validates'
export default (n1, n2) => {validateNumber(n1)
validateNumber(n2)
return n1 + n2
}
// somewhere/sum.spec.js
import sum from 'somewhere/sum'
import * as validates from 'somewhere/validates'
it('work for positive numbers', () => {
// mock validateNumber
const validateNumberMock = jest.spyOn(validates, 'validateNumber')
const result = sum(1, 2)
expect(result).toEqual(3)
// restore original implementation
validateNumberMock.mockRestore()})
咱们在下面测试代码中引入了源代码应用到的依赖 somewhere/validates
,这个时候就能够通过jest.spyOn
来 mock 这个依赖 export
的一些办法了,例如 validateNumber
。被 mock 的函数会在源代码被执行的时候应用,例如下面sum
执行的时候应用到的 validateNumber
就是咱们在 sum.spec.js
外面定义的 validateNumberMock
。这样咱们除了能够保障validateNumber
不会影响到咱们对 sum
函数逻辑的测试,还能够在里面对 validateNumberMock
进行一些断言(assertion)来验证 sum
逻辑的正确性。还有一点须要留神的是,我在测试用例执行完之后调用了 mockRestore
这个函数,这个函数会复原 validateNumber
函数原来的实现,从而防止这个测试用例对 validate
文件的更改影响到其它测试用例的正确执行。
我的项目引入 jest
理解完 jest 的一些根本 API 之后咱们再来看一下如何在咱们的我的项目外面引入 jest。
装置依赖
首先应用上面命令装置 jest
yarn add -D jest
如果你我的项目应用的是 Typescript,则还须要装置 ts-jest
作为依赖:
yarn add -D ts-jest
配置 jest
装置完 jest 后须要在 package.json 文件外面配置一下:
{
"jest": {
"transform": {"^.+\\.tsx?$": "ts-jest"},
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
"moduleDirectories": [
"node_modules",
"src"
],
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json",
"node"
]
}
}
下面各个配置项的意思别离是:
- transform: 通知 jest,你的 ts 或者 tsx 文件须要应用 ts-jest 来进行转换。
- testRegex: 通知 jest 哪些文件是须要被作为测试代码进行执行的,从下面的正则表达式咱们能够看出文件名中有 test 和 spec 的文件将会被作为测试用例执行。
- moduleDirectories: 通知 jest 在执行测试用例代码的时候,代码用到的 dependencies 应该去哪些目录进行 resolve,在这里 jest 会去
node_modules
和src
(或者你本人的源代码根目录)外面进行 resolve,这个应该要和你我的项目的 webpack.config.js 的 resolve 局部配置保持一致。 - moduleFileExtensions: 通知 jest 在找不到对应文件的时候应该尝试哪些文件后缀。
React hooks testing library
React-hooks-testing-library,是一个专门用来测试 React hook 的库。咱们晓得尽管 hook 是一个函数,可是咱们却不能用测试一般函数的办法来测试它们,因为它们的理论运行会波及到很多 React 运行时(runtime)的货色,因而很多人为了测试本人的 hook 会编写一些 TestComponent
来运行它们,这种办法非常不不便而且很难笼罩到所有的情景。为了简化开发者测试 hook 的流程,React 社区有人开发了这个叫做 react-hooks-testing-library
的库来容许咱们像测试一般函数一样测试咱们定义的 hook,这个库其实背地也是将咱们定义的 hook 运行在一个 TestComponent
外面,只不过它封装了一些繁难的 API 来简化咱们的测试。在开始应用这个库之前,咱们先来看一下它对外裸露的一些罕用的 API。
罕用 API
renderHook
renderHook
这个函数顾名思义就是用来渲染 hook 的,它会在调用的时候渲染一个专门用来测试的 TestComponent
来应用咱们的 hook。renderHook 的函数签名是 renderHook(callback, options?)
,它的第一个参数是一个callback
函数,这个函数会在 TestComponent
每次被从新渲染的时候调用,因而咱们能够在这个函数外面调用咱们想要测试的 hook。renderHook
的第二个参数是一个可选的 options
,这个options
能够带两个属性,一个是 initialProps
,它是TestComponent
的初始 props 参数,并且会被传递给 callback
函数用来调用 hook。options 的另外一个属性是 wrapper
,它用来指定TestComponent
的父级组件(Wrapper Component),这个组件能够是一些 ContextProvider
等用来为 TestComponent
的 hook 提供测试数据的货色。
renderHook
的返回值是 RenderHookResult
对象,这个对象会有上面这些属性:
- result:
result
是一个对象,它蕴含两个属性,一个是current
,它保留的是renderHook
callback
的返回值,另外一个属性是error
,它用来存储 hook 在 render 过程中呈现的任何谬误。 - rerender:
rerender
函数是用来从新渲染TestComponent
的,它能够接管一个 newProps 作为参数,这个参数会作为组件从新渲染时的 props 值,同样renderHook
的callback
函数也会应用这个新的 props 来从新调用。 - unmount:
unmount
函数是用来卸载TestComponent
的,它次要用来笼罩一些useEffect cleanup
函数的场景。
act
这函数和 React 自带的 test-utils 的 act 函数是同一个函数,咱们晓得组件状态更新的时候(setState),组件须要被从新渲染,而这个重渲染是须要 React 进行调度的,因而是个异步的过程,咱们能够通过应用 act
函数将所有会更新到组件状态的操作封装在它的 callback
外面来保障 act
函数执行完之后咱们定义的组件曾经实现了从新渲染。
装置
间接把 react-hooks-testing-library
作为咱们的我的项目devDependencies
:
yarn add -D @testing-library/react-hooks
留神:要应用 react-hooks-testing-library
咱们要确保咱们装置了 16.9.0
版本及其以上的 react
和react-test-renderer
:
yarn add react@^16.9.0
yarn add -D react-test-renderer@^16.9.0
例子
当初就让咱们看一个简略的同时应用 Jest
和react-hooks-testing-library
来测试 hook 的例子,如果咱们在我的项目外面定义了一个叫做 useCounter
的 Hook:
// somewhere/useCounter.js
import {useState, useCallback} from 'react'
function useCounter() {const [count, setCount] = useState(0)
const increment = useCallback(() => setCount(x => x + 1), [])
const decrement = useCallback(() => setCount(x => x - 1), [])
return {count, increment, decrease}
}
在下面的代码中我定义了一个叫做 useCounter
的 hook,这个 hook 是用来封装一个叫做 count 的状态并且对外裸露对 count 进行操作的一些 updater 包含 increment
和decrement
。如果大家对 useState
和useCallback
不够相熟的话能够看一下我的上一篇文章[React Hook 实战指南]()。接着就让咱们编写这个 hook 的测试用例:
// somewhere/useCounter.spec.js
import {renderHook, act} from '@testing-library/react-hooks'
import useCounter from 'somewhere/useCounter'
describe('Test useCounter', () => {describe('increment', () => {it('increase counter by 1', () => {const { result} = renderHook(() => useCounter())
act(() => {result.current.increment()
})
expect(result.current.count).toBe(1)
})
})
describe('decrement', () => {it('decrease counter by 1', () => {const { result} = renderHook(() => useCounter())
act(() => {result.current.decrement()
})
expect(result.current.count).toBe(-1)
})
})
})
下面的代码中咱们写了一个测试大组(describe)Test useCounter
并在这个大组外面定义了两个测试小组别离用来测试 useCounter
返回的 increment
和decrement
办法。咱们具体看一下形容为 increase counter by 1
的测试用例的代码,首先咱们要用 renderHook
函数来渲染要被测试的 hook,这里咱们须要将 useCounter
的返回值作为 callback
函数的返回值,这是因为咱们须要在里面拿到这个 hook 的返回后果 {count, increment, decrement}
。接着咱们应用act
函数来调用扭转组件状态 count
的increment
函数,act
函数实现之后咱们的组件也就实现了重渲染,前面就能够判断更新后的 count
是不是咱们想要的后果了。
总结
在本篇文章中我给大家介绍了什么叫做单元测试,为什么咱们须要在本人的我的项目外面引入单元测试以及教大家如何应用 Jest
和react-hooks-testing-library
来测试咱们自定义的 hook。
这篇文章是我的 React hook 系列文章的最初一篇了,前面我还会继续为大家分享一些和 hook 相干的内容,大家敬请期待。如果大家感觉对你有帮忙,欢送点赞和关注!
参考文献
- https://jestjs.io/
- https://react-hooks-testing-l…
集体技术动静
文章始发于我的集体博客
欢送关注公众号 进击的大葱 一起学习成长