在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.jsexport const functionWithCallback = (callback) => { callback(1, 2, 3)}// somewhere/functionWithCallback.spec.jsimport { 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.jsimport { validateNumber } from 'somewhere/validates'export default (n1, n2) => { validateNumber(n1) validateNumber(n2) return n1 + n2}// somewhere/sum.spec.jsimport 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.0yarn add -D react-test-renderer@^16.9.0
例子
当初就让咱们看一个简略的同时应用Jest
和react-hooks-testing-library
来测试hook的例子,如果咱们在我的项目外面定义了一个叫做useCounter
的Hook:
// somewhere/useCounter.jsimport { 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.jsimport { 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...
集体技术动静
文章始发于我的集体博客
欢送关注公众号进击的大葱一起学习成长