首先,我认为前端测试并非全部项目都必须的,由于写测试代码是须要要花费必定时间的,当项目比较简单的时候,花时间写测试代码可能反而会影响开发效率,可是须要指出的是,咱们前端开发过程当中,编写测试代码,有如下这些好处:javascript
TDD
和BDD
TDD
与单元测试TDD
所谓TDD(Test Driven Development)
,即测试驱动开发,简单的来讲就是先编写测试代码,而后以使得全部测试代码都经过为目的,编写逻辑代码,是一种以测试来驱动开发过程的开发模式。html
所谓单元测试(unit testing)
,是指对软件中的最小可测试单元进行检查和验证。通俗的讲,在前端,单元能够理解为一个独立的模块文件,单元测试就是对这样一个模块文件的测试。前端
对于一个独立的模块(ES6
模块),由于功能相对独立,因此咱们能够首先编写测试代码,而后根据测试代码指导编写逻辑代码。vue
因此提到TDD
,这里的测试通常是指单元测试java
BDD
与集成测试BDD
所谓BDD(Behavior Driven Development)
,即行为驱动开发,简单的来讲就是先编写业务逻辑代码,而后以使得全部业务逻辑按照预期结果执行为目的,编写测试代码,是一种以用户行为来驱动开发过程的开发模式。node
所谓集成测试(Integration Testing)
,是指对软件中的全部模块按照设计要求进行组装为完整系统后,进行检查和验证。通俗的讲,在前端,集成测试能够理解为对多个模块实现的一个交互完整的交互流程进行测试。react
对于多个模块(ES6
模块)组成的系统,须要首先将交互行为完善,才能按照预期行为编写测试代码。webpack
因此提到BDD
,这里的测试通常是指集成测试。ios
Jest
使用---引言部分若是咱们以前历来没有接触过测试代码,那让咱们本身来设计测试代码的写法,会是什么样呢?咱们须要让测试代码简单,通俗易懂,好比咱们举个例子以下:git
export function findMax (arr) { return Math.max(...arr) } 复制代码
咱们写了一个很简单的获取数组最大值的函数(你可能以为这样写并不严谨,但咱们为了简单,暂时假设输入是非空数值数组),若是对这个函数写一个测试其正确与否的测试程序,它可能构思是这样的:
我指望 findMax([1, 2, 4, 3]) 的结果是 4 复制代码
进一步转化为英文:
I expect findMax([1, 2, 4, 3]) to be 4 复制代码
用程序性的语言表示,expect
做为一个函数,为它传入想要测试的对象(findMax
函数),把输出结果也作一层封装toBe(4)
:
expect(findMax([1, 2, 4, 3])).toBe(4) // 有内味了 复制代码
更进一步,咱们想要增长一些描述性信息,好比
测试findMax函数,我指望 findMax([1, 2, 4, 3]) 的结果是 4 复制代码
这个时候,咱们能够再作一层封装,定义一个test
函数,它有两个参数,第一个参数是一些描述性信息(这里是 测试findMax
函数),第二个参数是一个函数,函数里能够执行咱们上面的逻辑,以下:
test('findMax函数输出', () => { expect(findMax([1, 2, 4, 3])).toBe(4) // 内味更深了 }) 复制代码
咱们本身能够简单的实现下test
函数和expect
函数,由于存在链式调用toBe
,因此expect
函数最终应该返回一个具备toBe
方法的对象,以下:
// expect函数 function expect (value) { return { toBe: (toBeValue) => { if (toBeValue === value) { console.log('测试经过!') } else { throw new Error('测试不经过!') } } } } // test函数 function test (msg, func) { try { func() console.log(`${msg}测试过程无异常!`) } catch (err) { console.error(`${msg}测试过程出错!`) } } 复制代码
咱们的测试方法,只是对数字作了简单的测试,实际项目中,须要测试的类型是不少的,这个时候咱们就能够选择一些比较成熟的测试框架。一个简单好用,功能强大的工具就呈如今咱们面前,它就是jest
。
Jest
使用---入门部分咱们这部分的例子主要是为了介绍jest
最基本的用法,首先咱们先简单的搭建一下演示环境。
第一步,使用npm init -y
(个人node
版本是v12.14.1
,npm
版本是v6.13.4
)初始化项目
第二步,安装jest
npm install --save-dev jest
(安装能够参考官网)
第三步,运行npx jest --init
命令,生成一份jest的配置文件jest.config.js
,个人选择以下
第四步,运行npm i babel-jest @babel/core @babel/preset-env -D
安装babel
,而且配置.babelrc
以下
{ presets: [ [ '@babel/preset-env', { targets: { node: 'current', }, }, ], ], }; 复制代码
第五步,根目录下创建src
文件夹,新建两个文件basic.js
和basic.test.js
第六步,package.json
增长一条命令:
"scripts": {
"test": "jest"
},
复制代码
以上六步完成后,咱们的项目结构应该以下图
jest
用法接下来咱们采用TDD
加单元测试的方式来学习jest基本用法:
首先,在basic.js
里定义两个工具函数
// 1. 寻找最大值 export function findMax (arr) { } // 2. 给定一个整数数组 nums 和一个目标值 target,在该数组中找出和为目标值的那 两个 整数,若是存在,返回true,不然返回false export function twoSum (nums, target) { }; 复制代码
既然是TDD
,咱们首先编写测试代码,在这个过程当中,咱们逐步学习各类jest
的基本用法。测试代码在basic.test.js
文件中编写:
import { findMax, twoSum } from './basic' // 指望findMax([2, 6, 3])执行后结果为6 test('findMax([2, 6, 3])', () => { expect(findMax([2, 6, 3])).toBe(6) }) // 指望twoSum([2, 3, 4, 6], 10)执行后结果为true test('twoSum([2, 3, 4, 6], 10)', () => { expect(twoSum([2, 3, 4, 6], 10)).toBe(true) }) 复制代码
从上面代码,咱们能够看到,jest
测试代码的写法,和以前咱们本身写的是同样的(固然啦,原本就是模仿jest
的),此时咱们运行npm test
命令,观察命令行输出以下:
Expected
表明指望函数执行的结果,也就是
toBe
里的那个值,
Received
表明实际执行函数获得的结果,由于咱们尚未编写业务代码,因此
Received
都是
undefined
,最后显示一共
1
个测试文件(
Test Suites
)和
2
条测试代码,它们都测试失败了。
接下来咱们完善basic.js
里的逻辑
// 1. 寻找最大值 export function findMax (arr) { return Math.max(...arr) } // 2. 给定一个整数数组 nums 和一个目标值 target,在该数组中找出和为目标值的那 两个 整数,若是存在,返回true,不然返回false export function twoSum (nums, target) { for (let i = 0; i < nums.length - 1; i++) { for (let j = i + 1; j < nums.length; j++) { if (nums[i] + nums[j] === target) { return true } } } return false }; 复制代码
而后咱们再次运行npm test
,获得结果以下
TDD
和单元测试的开发过程。
jest
matchers
像是上小节,在expect
函数后面跟着的判断结果的toBe
在jest
中被称为matcher
,咱们这一小节就来介绍另一些经常使用的matchers
toEqual
咱们首先改造下刚刚的twoSum
函数,让它返回找到的两个数的索引数组(leetcode
第一题)
// 2. 给定一个整数数组 nums 和一个目标值 target,在该数组中找出和为目标值的那 两个 整数, // 并返回他们的数组下标(假设每种输入只会对应一个答案,数组中同一个元素不能使用两遍)。 export function twoSum (nums, target) { for (let i = 0; i < nums.length - 1; i++) { for (let j = i + 1; j < nums.length; j++) { if (nums[i] + nums[j] === target) { return [i, j] } } } return [] }; 复制代码
接下来测试代码部分咱们只保留对twoSum
函数的测试,并同步修改测试代码
test('twoSum([2, 3, 4, 6], 10)', () => { expect(twoSum([2, 3, 4, 6], 10)).toBe([2, 3]) }) 复制代码
咱们的指望是函数执行的结果是[2, 3]
这样的数组,看起来没问题,运行npm test
咱们发现并无经过测试,这是由于,toBe
能够判断基本类型数据,可是对于数组,对象这样的引用类型是没办法判断的,这个时候,咱们就须要使用toEqual
test('twoSum([2, 3, 4, 6], 10)', () => { expect(twoSum([2, 3, 4, 6], 10)).toEqual([2, 3]) }) 复制代码
改为toEqual
以后,测试代码就成功了
matchers
这部份内容很简单,也比较多,因此直接在代码里注释说明:
test('变量a是否为null', () => { const a = null expect(a).toBeNull() }) test('变量a是否为undefined', () => { const a = undefined expect(a).toBeUndefined() }) test('变量a是否为defined', () => { const a = null expect(a).toBeDefined() }) test('变量a是否为true', () => { const a = 1 expect(a).toBeTruthy() }) test('变量a是否为false', () => { const a = 0 expect(a).toBeFalsy() }) 复制代码
测试结果以下:
not
修饰符很简单,not
就是对matcher
的否认
test('test not', () => { const temp = 10 expect(temp).not.toBe(11) expect(temp).not.toBeFalsy() expect(temp).toBeTruthy() }) 复制代码
测试结果以下:
matchers
这部份内容很简单,也比较多,因此直接在代码里注释说明:
// 判断数num是否大于某个数 test('toBeGreaterThan', () => { const num = 10 expect(num).toBeGreaterThan(7) }) // 判断数num是否大于等于某个数 test('toBeGreaterThanOrEqual', () => { const num = 10 expect(num).toBeGreaterThanOrEqual(10) }) // 判断数num是否小于某个数 test('toBeLessThan', () => { const num = 10 expect(num).toBeLessThan(20) }) // 判断数num是否小于等于某个数 test('toBeLessThanOrEqual', () => { const num = 10 expect(num).toBeLessThanOrEqual(10) expect(num).toBeLessThanOrEqual(20) }) 复制代码
测试结果以下:
上面介绍的都是整数判断,十分简单,可是若是是浮点数相关的判断,会不太同样,好比,咱们知道0.1 + 0.2 = 0.3
这个式子在数学中没有问题,可是在计算机中,因为精度问题,这个0.1 + 0.2
结果若是用toBe
结果并非准确的0.3
,若是咱们想要判断浮点数的相等,在jest
中提供了一个toBeCloseTo
的matcher
能够解决:
test('toBe', () => { const sum = 0.1 + 0.2 expect(sum).toBe(0.3) }) test('toBeCloseTo', () => { const sum = 0.1 + 0.2 expect(sum).toBeCloseTo(0.3) }) 复制代码
上面的测试结果以下:
toMatch
这个matcher
就是用来判断字符串是否和toMatch
提供的模式匹配,以下:
// 字符串相关 test('toMatch', () => { const str = 'Lebron James' expect(str).toMatch(/Ja/) expect(str).toMatch('Ja') }) 复制代码
matchers
可使用toContain
判断数组或者集合是否包含某个元素,使用toHaveLength
判断数组的长度,代码以下:
test('Array Set matchers', () => { const arr = ['Kobe', 'James', 'Curry'] const set = new Set(arr) expect(arr).toContain('Kobe') expect(set).toContain('Curry') expect(arr).toHaveLength(3) }) 复制代码
matchers
使用toThrow
来判断抛出的异常是否符合预期:
function throwError () { throw new Error('this is an error!!') } test('toThrow', () => { expect(throwError).toThrow(/this is an error/) }) 复制代码
jest
进阶用法所谓分组测试,核心在于,将不一样的测试进行分组,再结合勾子函数(生命周期函数),完成不一样分组的定制化测试,以知足测试过程重的复杂需求。
咱们首先在src
下新建两个文件hook.js
和hook.test.js
,这一部分代码在这两个文件中完成,首先直接给出hook.js
代码
// hook.js export default class Count { constructor () { this.count = 2 } increase () { this.count ++ } decrease () { this.count -- } double () { this.count *= this.count } half () { this.count /= this.count } } 复制代码
如今呢,咱们想要对Count
类的四个方法单独测试,数据互相不影响,固然咱们能够本身去直接实例化4
个对象,不过,jest
给了咱们更优雅的写法---分组,咱们使用describe
函数分组,以下:
describe('分别测试Count的4个方法', () => { test('测试increase', () => { }) test('测试decrease', () => { }) test('测试double', () => { }) test('测试half', () => { }) }) 复制代码
这样咱们就使用describe
函数配合test
将测试分为了四组,接下来,为了能更好的控制每一个test
组,咱们就要用到jest
的勾子函数。 咱们这里要介绍的是jest
里的四个勾子函数beforeEach,beforeAll,afterEach,afterAll
。
顾名思义,beforeEach
是在每个test函数执行以前,会被调用;afterEach
则是在每个test函数执行以后调用;beforeAll
是在全部test函数执行以前调用;afterAll
则是在全部test函数执行以后调用。咱们能够看下面这个例子:
import Count from "./hook" describe('分别测试Count的4个方法', () => { let count beforeAll(() => { console.log('before all tests!') }) beforeEach(() => { console.log('before each test!') count = new Count() }) afterAll(() => { console.log('after all tests!') }) afterEach(() => { console.log('after each test!') }) test('测试increase', () => { count.increase() console.log(count.count) }) test('测试decrease', () => { count.decrease() console.log(count.count) }) test('测试double', () => { count.double() console.log(count.count) }) test('测试half', () => { count.half() console.log(count.count) }) }) 复制代码
输出的结果如图:
能够看到,咱们在每一个test执行以前,beforeEach
里面从新实例化了count
,因此每一次的count是不一样的。合理的使用勾子函数,咱们能够更好的定制测试。
在咱们前端开发过程当中,因为javascript
是单线程的,异步编程是咱们开发人员常常要作的工做,而异步代码也是最容易出错的地方,对异步代码逻辑进行测试,是颇有必要的,这一节将对jest
如何进行异步测试,作一个详细的介绍。
setTimeout
开始咱们首先新建timeout.js,timeout.test.js
文件,timeout.js
文件代码很简单:
export default (fn) => { setTimeout(() => { fn() }, 2000) } 复制代码
咱们如今的目标就是去测试,写的这个函数,是否是会像咱们预期的那样,传入一个函数做为参数(简单为主,没有作参数校验),2s
后,执行这个函数。
咱们的测试代码(timeout.test.js
)以下:
import timeout from './timeout' test('测试timer', () => { timeout(() => { expect(2+2).toBe(4) }) }) 复制代码
若是咱们运行这段测试代码,必定是会经过的,可是,这真的表明咱们写在timeout
里的方法测试经过了吗?咱们在timout.js
中打印输出一段文字
export default (fn) => { setTimeout(() => { fn() console.log('this is timeout!') }, 2000) } 复制代码
而后咱们运行测试代码(npm test timeout.test
这样只运行一个文件),你会发现,什么打印内容都没有输出:
jest
在运行测试代码,执行
test
方法时,从函数内部第一行执行到最后一行,当执行逻辑走到代码块最后一行时,没有异常就会返回测试成功,这个过程当中
不会去等待异步代码的执行结果
,因此咱们这样的测试方法,无论
setTimeout
里怎么实现,回调函数里怎么实现,都不会执行回调函数内部的逻辑。
若是咱们须要测试代码在真正执行了定时器里的异步逻辑后,才返回测试结果,咱们须要给test
方法的回调函数传入一个done
参数,并在test
方法内异步执行的代码中调用这个done
方法,这样,test
方法会等到done
所在的代码块内容执行完毕后才返回测试结果:
import timeout from './timeout' test('测试timer', (done) => { timeout(() => { expect(2+2).toBe(4) done() }) }) 复制代码
done
参数以后,获得了预期的结果,打印输出了内容,证实咱们回调函数内的代码执行了。
fakeTimers
提升测试效率咱们上一小节介绍了如何去测试写在定时器里异步代码的执行,但这里存在一个问题,好比,咱们的定时器可能须要几十秒才执行内部逻辑(这虽然不多见,主要看业务需求),咱们的测试代码也会好久才会返回结果,这无疑大大的下降了开发测试效率。
jest
也考虑到了这一点,让咱们可使用fakeTimers
模拟真实的定时器。这个fakeTimers
在遇到定时器时,容许咱们当即跳过定时器等待时间,执行内部逻辑,好比,对于刚刚的timeout.test
,咱们的测试代码能够作以下改变:
jest.fn()
生成一个jest
提供的用来测试的函数,这样咱们以后回调函数不须要本身去写一个jest.useFakeTimers()
方法启动fakeTimers
jest.advanceTimersByTime()
方法,参数传入毫秒时间,jest
会当即跳过这个时间值,还能够经过toHaveBeenCalledTimes()
这个mathcer
来测试函数的调用次数。完整代码以下:
test('测试timer', () => { jest.useFakeTimers() const fn = jest.fn() timeout(fn) // 时间快进2秒 jest.advanceTimersByTime(2000) expect(fn).toHaveBeenCalledTimes(1) }) 复制代码
测试timer(12ms)
对比以前的
测试timer(2021ms)
,能够看到,定时器的延迟时间,确实被跳过了,这提升了测试开发效率。
通过前面两节的介绍,对于定时器这种异步场景的测试代码编写,实际上咱们已经掌握核心内容,这一节,咱们去探讨一个更为复杂的场景,那就是定时器嵌套。
咱们首先改造timout
里的代码以下:
export default (fn) => { setTimeout(() => { fn() console.log('this is timeout outside!') setTimeout(() => { fn() console.log('this is timeout inside!') }, 3000) }, 2000) } 复制代码
按照上一小节的写法,咱们的测试代码能够改造为:
test('测试timer', () => { jest.useFakeTimers() const fn = jest.fn() timeout(fn) // 时间快进2秒 jest.advanceTimersByTime(2000) expect(fn).toHaveBeenCalledTimes(1) // 时间快进3秒 jest.advanceTimersByTime(3000) expect(fn).toHaveBeenCalledTimes(2) }) 复制代码
其实也很简单,就是在第一次2s
后,再过3s
后执行第二个定时器,此时fn
被调用了2
次,因此咱们只须要加上最后两行代码就能够了。执行结果以下:
咱们能够看到,两条打印结果都输出了。可是目前的这种实现不是很好,试想一下,若是这里面的定时器嵌套比较多,或者咱们不清楚有几个定时器,就会比较麻烦。jest
为这种状况提供了两个有用的方法:
jest.runAllTimers()
这个方法就如同它的名字同样,调用以后,会执行全部定时器,咱们的代码能够改造以下:
test('测试timer', () => { jest.useFakeTimers() const fn = jest.fn() timeout(fn) jest.runAllTimers() expect(fn).toHaveBeenCalledTimes(2) }) 复制代码
jest
依旧快速的跳过了定时器等待时间。
jest.runOnlyPendingTimers()
这个方法的意思是,只执行当前正在等待的全部定时器,这个例子中,只有外层定时器是正在等待的,内层定时器只有在外层定时器执行时,才处于等待状态,咱们改造测试代码以下:
test('测试timer', () => { jest.useFakeTimers() const fn = jest.fn() timeout(fn) jest.runOnlyPendingTimers() expect(fn).toHaveBeenCalledTimes(1) }) 复制代码
jest.runOnlyPendingTimers()
便可。
关于上述内容,有一点须要说明:
若是咱们编写了多个test
函数,它们都使用fakeTimers
,必定要在beforeEach
勾子中每次都调用jest.useFakeTimers()
,不然,多个test
函数中的fakeTimers
会是同一个,将会互相干扰,产生不符合预期的执行结果
beforeEach(() => { jest.useFakeTimers() }) 复制代码
(promise/async await)
promise
写法在咱们前端开发中,经过请求后端接口获取数据是很重要的一个流程,这一节主要就是介绍这个过程当中如何编写测试代码(实际上这里的不少内容,以前介绍定时器的章节是有介绍过的)
为了简单起见,咱们使用axios(npm i axios)
这个成熟的库来辅助咱们作数据请求。首先新建request.js, request.test.js
这两个文件,在request.js
文件请求一个免费api:
import axios from 'axios' export const request = fn => { axios.get('https://jsonplaceholder.typicode.com/todos/1').then(res => { fn(res) console.log(res) }) } 复制代码
咱们在request.test.js
中,为了保证异步代码执行完毕后结束测试,和以前介绍的同样,在test
的回调函数中传入done
参数,在回调函数里执行done()
,代码以下:
import { request } from './request' test('测试request', (done) => { request(data => { expect(data.data).toEqual({ "userId": 1, "id": 1, "title": "delectus aut autem", "completed": false }) done() }) }) 复制代码
咱们如今改造一下request.js
的代码,让它返回一个promise
:
export const request = () => { return axios.get('https://jsonplaceholder.typicode.com/todos/1') } 复制代码
为了测试上述代码,咱们request.test.js
也要作必定的修改:
test('测试request', () => { return request().then(data => { expect(data.data).toEqual({ "userId": 1, "id": 1, "title": "delectus aut autem", "completed": false }) }) }) 复制代码
注意,上面的写法不须要传入done
参数了,可是,须要咱们使用return
返回,若是不写return
,那jest
执行test
函数时,将不会等待promise
返回,这样的话,测试结果输出时,then
方法将不会执行。咱们能够尝试如下两种写法(改变"completed": true
),第一种写法测试不会经过,第二种测试是能够经过的(由于promise并无返回结果):
// 第一种 test('测试request', () => { return request().then(data => { expect(data.data).toEqual({ "userId": 1, "id": 1, "title": "delectus aut autem", "completed": true }) }) }) 复制代码
// 第二种 test('测试request', () => { request().then(data => { expect(data.data).toEqual({ "userId": 1, "id": 1, "title": "delectus aut autem", "completed": true }) }) }) 复制代码
上面的测试代码,咱们也能够写成下面的形式:
test('测试request', () => { return expect(request()).resolves.toMatchObject({ data: { "userId": 1, "id": 1, "title": "delectus aut autem", "completed": false } }) }) 复制代码
注意,resolves
将返回request
方法执行后全部返回内容,咱们使用toMatchObject
这个matcher
,当传入的对象可以匹配到request
方法执行后返回对象的一部分键值对,测试就会经过。
async await
语法糖async await
本质上就是promise
链式调用的语法糖,咱们上一小节最后的测试代码,若是使用async await
的方式去书写,以下:
// 写法一 test('测试request', async () => { const res = await request() expect(res.data).toEqual({ "userId": 1, "id": 1, "title": "delectus aut autem", "completed": false }) }) // 写法二 test('测试request', async () => { await expect(request()).resolves.toMatchObject({ data: { "userId": 1, "id": 1, "title": "delectus aut autem", "completed": false } }) }) 复制代码
在咱们实际项目中,须要对这种接口请求作错误处理,一样,也须要对异常状况编写测试代码。
咱们首先在request.js
增长一个方法:
export const requestErr = fn => { return axios.get('https://jsonplaceholder.typicode.com/sda') } 复制代码
这里请求一个不存在的接口地址,会返回404
,因而咱们的的测试代码为:
test('测试request 404', () => { return requestErr().catch((e) => { console.log(e.toString()) expect(e.toString().indexOf('404') > -1).toBeTruthy() }) }) 复制代码
jest
官网的一段说明:
catch
,jest不回去执行
catch
里的内容,因此须要咱们去写
expect.assertions(1)
这句话,表明,指望执行的断言是1次,
catch
方法算一次断言,因此,正常状况,因为不会执行
catch
,这里会报错(执行了0次断言),当这里报错了,说明咱们的代码也按照预期产生了异常。
这种写法目前已经不须要了,详细见removed useless expect.assertions,因此,如今就按照上面那种方式,直接书写,测试经过表明确实如咱们预期的产生异常。
一样的,咱们还可使用另外一种方式完成异常代码测试:
test('测试request 404', () => { return expect(requestErr()).rejects.toThrow(/404/) }) 复制代码
这里的rejects
和上一节的resolves
相互对于,表明执行方法产生的错误对象,这个错误对象抛出404
异常(toThrow(/404/)
)
咱们一样可使用async await
语法糖书写异常测试的代码:
test('测试request 404', async () => { await expect(requestErr()).rejects.toThrow(/404/) }) // 或者可使用try catch语句写的更完整 test('测试request 404', async () => { try { await requestErr() } catch (e) { expect(e.toString()).toBe('Error: Request failed with status code 404') } }) 复制代码
mock
)数据咱们首先新建mock.js, mock.test.js
文件
jest.fn()
模拟函数首先在mock.js
写一个函数:
export const run = fn => { return fn('this is run!') } 复制代码
实际上以前咱们已经使用过jest.fn()
了,这里咱们更进一步的学习它。
fn()
函数能够接受一个函数做为参数,这个函数就是咱们想要jest.fn()
为咱们mock
的函数,咱们编写mock.test.js
:test('测试 jest.fn()', () => { const fn = jest.fn(() => { return 'this is mock fn 1' }) }) 复制代码
jest.fn()
能够初始化时候不传入参数,而后经过调用生成的mock
函数的mockImplementation
或者mockImplementationOnce
方法来改变mock函数内容,这两个方法的区别是,mockImplementationOnce
只会改变要mock
的函数一次:test('测试 jest.fn()', () => { const func = jest.fn() func.mockImplementation(() => { return 'this is mock fn 1' }) func.mockImplementationOnce(() => { return 'this is mock fn 2' }) const a = run(func) const b = run(func) const c = run(func) console.log(a) console.log(b) console.log(c) }) 复制代码
this is mock fn 2
,以后都是
this is mock fn 1
一样的,咱们可使用mock
函数的mockReturnValue
和mockReturnValueOnce(一次)
方法来改变函数的返回值:
test('测试 jest.fn()', () => { const func = jest.fn() func.mockImplementation(() => { return 'this is mock fn 1' }) func.mockImplementationOnce(() => { return 'this is mock fn 2' }) func.mockReturnValue('this is mock fn 3') func.mockReturnValueOnce('this is mock fn 4') .mockReturnValueOnce('this is mock fn 5') .mockReturnValueOnce('this is mock fn 6') const a = run(func) const b = run(func) const c = run(func) const d = run(func) console.log(a) console.log(b) console.log(c) console.log(d) }) 复制代码
注意到,方法是能够链式调用的,方便屡次输出不一样的返回值。
toBeCalledWith
这个matcher
来测试函数的传参数是否符合预期:test('测试 jest.fn()', () => { const func = jest.fn() const a = run(func) expect(func).toBeCalledWith('this is run!') }) 复制代码
不少时候,咱们在前端开发过程当中,后端接口尚未提供,咱们须要去mock
接口返回的数据。
咱们首先在mock.js
中编写一个简单的请求数据的代码:
import axios from 'axios' export const request = fn => { return axios.get('https://jsonplaceholder.typicode.com/todos/1') } 复制代码
接着,咱们在mock.test.js
中,使用jest.mock()
方法模拟axios
,使用mockResolvedValue
和mockResolvedValueOnce
方法模拟返回的数据,一样的,mockResolvedValueOnce
方法只会改变一次返回的数据:
import axios from 'axios' import { request } from './mock' jest.mock('axios') test('测试request', async () => { axios.get.mockResolvedValueOnce({ data: 'Jordan', position: 'SG' }) axios.get.mockResolvedValue({ data: 'kobe', position: 'SG' }) await request().then((res) => { expect(res.data).toBe('Jordan') }) await request().then((res) => { expect(res.data).toBe('kobe') }) }) 复制代码
咱们使用jest.mock('axios')
来使用jest
去模拟axios
,测试正确的经过了。
dom
相关测试dom
相关的测试其实很简单,咱们首先新建dom.js, dom.test.js
两个文件,代码以下:
// dom.js export const generateDiv = () => { const div = document.createElement('div') div.className = 'test-div' document.body.appendChild(div) } // dom.test.js import { generateDiv } from './dom' test('测试dom操做', () => { generateDiv() generateDiv() generateDiv() expect(document.getElementsByClassName('test-div').length).toBe(3) }) 复制代码
这里只有一点要说明,jest
的运行环境是node.js
,这里jest
使用jsdom
来让咱们能够书写dom
操做相关的测试逻辑。
snapshot
)测试咱们若是没有接触过快照测试,可能会以为这个名字很高大上。因此咱们首先新建snapshot.js, shapshot.test.js
来看看快照测试到底是什么。
在咱们的平常开发中,总会写一些配置性的代码,它们大致不会变化,可是也会有小的变动,这样的配置可能以下(snapshot.js
):
export const getConfig = () => { return { server: 'https://demo.com', port: '8080' } } 复制代码
咱们的测试代码以下:
import { getConfig } from './snapshot' test('getConfig测试', () => { expect(getConfig()).toEqual({ server: 'https://demo.com', port: '8080' }) }) 复制代码
这样咱们经过了测试。可是,假如后续咱们的配置改变了,我就须要同步的去修改测试代码,这样会比较麻烦,从而,jest
为咱们引入了快照测试,先上测试代码:
test('getConfig测试', () => { expect(getConfig()).toMatchSnapshot() }) 复制代码
咱们运行测试代码以后,会在项目根目录下生成一个__snapshots__
文件夹,下面有一个snapshot.test.js.snap
快照文件,文件内容以下:
// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`getConfig测试 1`] = ` Object { "port": "8080", "server": "https://demo.com", } `; 复制代码
jest
会在运行toMatchSnapshot()
的时候,首先检查有没有这个快照文件,若是没有,则生成,当咱们改动配置内容时,好比把port
改成8090
,再次运行测试代码,测试不经过,结果以下:
npm test snapshot.test -- -u
,就能够自动更新咱们的快照文件,测试再次经过,这就让咱们不须要每次更改配置文件的时候,手动去同步更新测试代码,提升了测试开发效率:
此时咱们的快照文件更新为以下代码:
// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`getConfig测试 1`] = ` Object { "port": "8090", "server": "https://demo.com", } `; 复制代码
jest
其它一些有用的知识jest
监听文件变化这个功能很简单,咱们只须要运行jest
命令的时候,后面加上--watch
便可,咱们在package.json
中新增一条命令:
"scripts": {
"test": "jest",
"test-watch": "jest --watch"
},
复制代码
在新增完这条命令后,为了能让jest
能够监听文件变化,咱们还须要把咱们的代码文件变成一个git
仓库,jest
也正式依靠git
的能力实现监听文件变化的,咱们运行git init
,接着咱们运行npm run test-watch
,在必定时间后,咱们开启监听模式,命令行最后几行输出应该是:
这里对watch
模式的几个有用功能作一个简单介绍(也就是图中英文说明):
a
键运行全部测试代码f
键只运行全部失败的测试代码p
键按照文件名筛选测试代码(支持正则)t
键按照测试名筛选测试代码(支持正则)q
键盘推出watch
模式enter
键触发一次测试运行这些我建议你们自行去尝试,它们都是十分简单好用的功能。
测试覆盖率,简单来讲就是咱们业务代码中,编写测试代码的比例,jest
给咱们提供了直接生成测试覆盖率文件的方法,也就是运行jest
命令时后面加上--coverage
参数,咱们修改package.json
文件以下:
"scripts": {
"test": "jest",
"test-watch": "jest --watch",
"coverage": "jest --coverage"
},
复制代码
接下来,运行npm run coverage
,咱们能够看到命令行输出以下:
coverage
文件夹:
index.html
,以下图:
这里对这个表格项目作一个简单的说明:
Statements
是语句覆盖率:表示代码中有多少执行的语句被测试到了
Branches
是分支覆盖率:表示代码中有多少if else switch
分支被测试到了
Functions
是函数覆盖率:表示代码中被测试到的函数的占比
Lines
是行覆盖率:表示代码中被测试到的行数占比
咱们能够利用生成的测试覆盖率文件,更好的完善改进咱们的测试代码。
jest.config.js
配置文件我对于学习一个工具的配置文件的建议是,首先按照默认的来,当你须要改变配置的时候,再去查阅官方文档学习,不推荐去死记硬背。
我这里也不会去介绍怎么去配置jest
文件,咱们能够经过jest初始化时候默认生成的那个jest.config.js
来学习(有详细注释),也能够在官网中查阅相关的配置参数。
因为篇幅缘由,不适合再介绍更多的信息,更多的api
相关的信息,建议去查阅官网来学习。
这篇文章我的认为已经把jest
的基础和最核心的内容作了阐述,可能咱们开发过程当中,使用react(enzyme), vue( @vue/test-utils)
这样的开发框架,使用webpack
这样的工程化工具,在使用jest
的时候,会结合使用一些开源库,我相信学好了jest
自己以后,配置和使用它们都不会有太多困难。
最后,但愿这篇文章能够帮助到你们,感谢能看到这里的每个小伙伴。