首先是为何要写单元测试? 主要仍是测试咱们代码有没有达到预期的效果,其次,若是严格按照TDD(测试驱动开发)来进行开发的话,咱们还会更加注重产品细节,代码可能更加健壮。由于TDD是测试放到第一位,写代码以前,先写测试。测试怎么写?确定是思考产品的各类使用场景,以及在每种场景下,会有什么效果或异常,思考好了,测试写完了,整个产品也就是一清二楚了。真正写代码的时候,只是把测试场景用代码实现,这些所谓的场景就是一个个测试用例。html
其次是怎么写单元测试。测试的目的就是看看代码有没有达到咱们的预期,那确定是先引入要测试的代码,而后是运行代码,最后再和咱们的预期作比较,若是和预期一致,就代表代码没有问题,若是没有达到预期, 就是代码有问题。和预期进行比较,就是断言。这也就是意味着,每个测试中最终都会落脚到一个断言。若是没有断言(比较),测试也没有意义,代码运行完了,结果呢?不知道。断言就是比较,判断正不正确,对不对,1 + 1 是否是等于2, 就是一个最简单的断言,1+1是计算,程序的运行,2就是指望或预期,等于就是判断或比较,具体到测试代码(jest)中,就是expect(1+1).toBe(2). 在测试程序中, 只要看到expect 什么, to 什么,这就是断言,还有多是assert.node
最后是运行测试代码。单元测试写了,确定要运行,不运行,怎么知道结果。这里是我学习时最大的困惑,想明白了,有些问题也就迎刃而解了。当咱们运行单元测试,就是在命令行中输入jest的时候,其实是在node 环境下执行js代码。使用jest 进行单元测试,就是启动了一个node程序来执行测试代码。每当遇到测试问题的时候,先想想这个前提,说不定,问题就解决了。git
好了,说的也差很少了,那就一块儿来学习一下Jest 吧。Jest有一个好处,就是不用配置也能用,开箱即用,它提供了断言,函数的mock等经常使用的测试功能。npm install jest --save-dev, 安装jest 以后,就可使用它进行单元测试了。打开git bash,输入mkdir jest-test && cd jest-test && npm init -y, 新建jest-test 项目并初始化为node项目。再npm install jest --save-dev, 安装jest ,如今就能够进行测试了。先从简单的函数测试开始学起。touch func.js, 新建一个func.js 文件,暴漏一个greeting 函数,注意使用commonJs 的格式,由于jest 是在node环境下运行的,node暂时没有实现ES6 moduleajax
function greeting(guest) { return `Hello ${guest}`; }
函数写完了,那怎么测试呢,测试代码放到什么地方呢?Jest识别三种测试文件,以.test.js结尾的文件,以.spec.js结尾的文件,和放到__tests__ 文件夹中的文件。Jest 在进行测试的时候,它会在整个项目进行查找,只要碰到这三种文件它都会执行。干脆,再写两个函数,用三种测试文件分别进行测试, func.js 以下npm
function greeting(guest) { return `Hello ${guest}`; } function createObj(name, age) { return { name, age } } function isTrueOrFasle(bool) { return bool } module.exports = { greeting, createObj, isTrueOrFasle }
新建greeting.test.js测试greeting 函数,createObj.spec.js来测试createObj函数,新建一个__tests__ 文件夹,在里面建一个isTrue.js 来测试isTrueOrFalse 函数。 具体到测试代码的书写,jest 也有多种方式,能够直接在测试文件中写一个个的test或it用来测试,也可使用describe 函数,建立一个测试集,再在describe里面写test或it , 在jest中,it和test 是同样的功能,它们都接受两个参数,第一个是字符串,对这个测试进行描述,须要什么条件,达到什么效果。第二个是函数,函数体就是真正的测试代码,jest 要执行的代码。来写一下greeting.test.js 文件,greeting 函数的做用就是 传入guest参数,返回Hello guest. 那对应的一个测试用例就是 传入sam,返回Hello sam. 那描述就能够这么写, should return Hello sam when call greeting with param sam, 具体到测试代码,引入greeting 函数,调用greeting 函数,传入‘sam’ 参数, 做一个断言,函数调用的返回值是否是等于Hello sam. greeting.test.js 以下json
const greeting = require('./fun').greeting; test('should return Hello sam when input sam', () => { let result = greeting('sam'); expect(result).toBe('Hello sam'); })
这和文章开始说的同样,测试的写法为三步,引入测试内容,运行测试内容,最后作一个断言进行比较,是否达到预期。Jest中的断言使用expect, 它接受一个参数,就是运行测试内容的结果,返回一个对象,这个对象来调用匹配器(toBe) ,匹配器的参数就是咱们的预期结果,这样就能够对结果和预期进行对比了,也就能够判断对不对了。按照greeting测试的写法,再写一下createObj的测试,使用it数组
const createObj = require('./fun').createObj; it('should return {name: "sam", age: 30} when input "sam" and 30', () => { let result = createObj('sam', 30); expect(result).toEqual({name: 'sam', age: 30}); // 使用toEqual })
最后是isTrueOrFalse函数的测试,这里最好用describe(). 由于这个测试分为两种状况,一个it 或test搞不定。对一个功能进行测试,但它分为多种状况,须要多个test, 最好使用descibe() 把多个test 包起来,造成一组测试。只有这一组都测试完成以后,才能说明这个功能是好的。它的语法和test 的一致,第一个参数也是字符串,对这一组测试进行描述, 第二个参数是一个函数,函数体就是一个个的test 测试。promise
const isTrueOrFasle = require('../fun').isTrueOrFasle; describe('true or false', () => { it('should return true when input true', () => { let result = isTrueOrFasle(true); expect(result).toBeTruthy(); // toBeTruthy 匹配器 }) test('should return false when input fasle', () => { let result = isTrueOrFasle(false); expect(result).toBeFalsy(); // toBeFalsy 匹配器 }) })
三个测试写完了,那就运行一下,看看对不对。把package.json中的scripts 的test 字段的值改为 'jest', 而后npm run test 进行测试, 能够看到三个测试都经过了。 修改一下,让一个测试不经过,好比isTrue.js中把第一个改为false,bash
it('should return true when input true', () => { let result = isTrueOrFasle(false); expect(result).toBeTruthy(); // toBeTruthy 匹配器 })
再运行npm run test ,服务器
能够看到失败了,也指出了失败的地方,再看一下它的描述,它把组测试放到前面,后面是一个测试用例的描述,这样,咱们就很轻松看到哪个功能出问题了,而且是哪个case. 这也是把同一个功能的多个test case 放到一块儿的好处。
咱们再把它改回去,再执行npm run test,若是这样改动测试,每一次都要执行测试的时候,使用npm run test就有点麻烦了,jest 提供了一个watchAll 参数,会对测试文件以及测试文件引用的源文件进行实时监听,若是有变化,当即进行测试。package.json中的 test 改为成jest --watchAll
"scripts": { "test": "jest --watchAll" }
npm run test, 就能够启动jest 的实时测试了。固然你也能够随时中止掉,按q 键就能够。
jest 的基本测试差很少了,再来看看它的异步代码的测试, 先把全部的测试文件删掉,再新建一个func.test.js 文件,如今就只有func.js 和 func.test.js 了。处理异步或是用回调函数, 或是promise 。
回调函数
最多见的回调函数就是ajax请求,返回数据后执行成功或失败的回调。在Node 环境下,有一个npm 包request, 它能够发送异步请求,返回数据后调用回调函数进行处理,npm i request --save, 安装一下,而后func.js 修改以下
const request = require('request'); function fetchData(callback) { request('https://jsonplaceholder.typicode.com/todos/1', function (error, response, body) { callback(body); }); } module.exports = fetchData;
那怎么测试?确定调用fetchData, 那就先要建立一个回调函数传给它,由于fetchData获取到数据后,会调用回调函数,那就能够在回调函数中建立一个断言,判断返回的数据是否是和指望的同样。func.test.js 文件修改成以下测试代码。
const fetchData = require('./fun'); test('should return data when fetchData request success', () => { function callback(data) { expect(data).toEqual({ "userId": 1, "id": 1, "title": "delectus aut autem", "completed": false }) } fetchData(callback); })
执行npm run test 命令,看到pass,但其实并无达到预期的效果,在callback 函数中加入console.log(data) 试一试,就知道了。测试文件改变后,jest 会从新跑一遍(开启了watch),但并无打印出data,也就是说callback 函数并无被调用。这是为何,我在这个地方想了很久,主要仍是对异步了解的不够。当执行jest 测试的时候,其实是node 执行test函数体的代码,首先看到callback的函数声明,它声明函数,而后看到fetchData() ,它就调用这个函数,请求https://jsonplaceholder.typicode.com/todos/1 接口,这个时候,getTodo函数就执行完了。你可能会想,回调函数都没有执行,这个函数怎么算执行完了呢?回调函数并非代码执行的,而是放到node的异步队列中被执行的。异步的请求,能够看做是一个对话,
执行fetchData: " hi, node, 你帮我执行一个请求,若是请求成功,就执行这个回调函数"
node: "好,我帮你请求”
而后node 就请求了,而后实时监听请求的状态,若是返回数据,它就把回调函数插入到它的异步队列中。Node的事件循环机制,就把这个函数执行了。
这时再看异步函数,其实,异步函数的做用,只是一个告知的做用,告知环境来帮我作事情,只要告知了,函数就算执行完了,其它剩下的事情,请求接口,执行回调函数,就是环境的事了。
只要一告知,getTodo 函数就执行完了,继续向下执行,因为函数的执行是该测试的最后一行代码,它执行完以后,这个测试就执行完了,没有错误,jest 就pass了. 可是该测试并无覆盖到callback函数的调用,实际上在背后,node是帮咱们发送请求,执行callback 的。这也就是官网说的,By default, Jest tests complete once they reach the end of their execution. That means this test will not work as intended: The problem is that the test will complete as soon as fetchData
completes, before ever calling the callback. 那怎么办,官方的建议是使用done. 就是test的第二个参数接受一个done, 而后在callback 里面加done(), 以下所示
test('should return data when fetchData request success', (done) => { function callback(data) { console.log(data); expect(data).toEqual({ "userId": 1, "id": 1, "title": "delectus aut autem", "completed": false }) done(); } fetchData(callback); });
测试又从新跑了一遍,仍是报错了,不过是断言写错了,代表callback 调用了,达到了预期的效果。data 是一个字符串,toEqual了一个对象,因此测试失败了。Json parse 一下data 就能够了。
expect(JSON.parse(data)).toEqual({ "userId": 1, "id": 1, "title": "delectus aut autem", "completed": false })
测试经过了。思考一下done 的做用,加上done 表示等待,jest 在执行单元测试的时候,若是它看到一个测试用例(test) 的第二个函数参数有一个done参数, 它就知道,这个测试用例,执行完最后一行代码的时候,还不算测试完成,还要等待,等待done函数的执行,只有done函数执行了,这个测试用例才算完成,才能执行下一个test. 若是done函数没有调用,它就不能执行下一个测试(test)。加上done 之后,一个测试有没有执行完的依据就不是执行到最后一行代码,而是看done() 有没有被调用。也就是说,done 改变了测试的默认行为,由于默认状况下,只要执行到一个测试的最后一行代码,就认为个测试执行完了,就要执行下一个测试,但有了done就不同了,它执行完这一行代码后,并不会继续执行下一个测试,而是等待,等待done() 函数的执行,只有done()函数执行了,它才会执行下一个测试。具体到这个测试用例,当jest执行到fetchData(callback) 这一行代码的时候,他就暂停执行了,这时node 就会在背后发送请求,请求成功后,把回调函数放到异步队列中,node的事件循环机制就会执行这个回调函数,而回调函数中正好有done(), done() 函数执行了,这时jest 看到done函数执行了,就把测试置为pass, 而后执行下一个测试。固然jest也不会一直等着,默认是5s,若是5s后done 尚未执行,它就执行下一个测试,这也代表测试失败了。有时,网落太慢或没有网的时候, 你再跑这个测试,你会现以下错误
这就是5s内没有调用 done() 测试失败的例子。
总结一下,异步回调函数的测试,一个是使用done做为参数,一个是调用done,在测试的某个地方必定要触发或者调用done()。done是针对一个个test测试用例而言的,目的就是告诉jest一个个test 测试真正完成依据是什么。若是一个测试有done参数,就表示这个测试完成的依据是done的调用,执行到测试的最后一行代码,也不算完事,只有done调用了,才算完事,没有办法,jest在执行这个测试的时候,就只能等待done的调用,只有调用了,它才会执行一个测试。若是一个测试没有done参数,那么这个测试的完成的依据就是执行完最后一行代码,执行完最后一行代码,jest就能够执行下一个测试了。 固然也不会一直等待,默认是5s。
Promise
Promise 相对好测试一点,由于promise 可使用then的链式调用。只要等待它的resolve, 而后调用then 来接受返回的数据进行对比就能够了,若是没有resolve 确定是失败了。等待resolve,在测试中是使用的return, return Promise 的调用,就是等待它的resolve. 把fetchData 函数转化成使用promise 进行请求,func.js以下
const request = require('request'); function fetchData() { return new Promise((resolve, reject) => { request('https://jsonplaceholder.typicode.com/todos/1', function (error, response, body) { if (error) { reject(error); } resolve(body); }); }) } module.exports = fetchData;
测试函数(func.test.js)改成
test('should return data when fetchData request success', () => { return fetchData().then(data => { expect(JSON.parse(data)).toEqual({ "userId": 1, "id": 1, "title": "delectus aut autem", "completed": false }) }) });
进行promise测试的时候,在测试代码中,必定要注意使用return, 若是没有return,就没有等待,没有等待,就没有resolve,then 也就不会执行了,测试效果达不到。若是你想测试error, 把测试代码改为error ,
test('should return err when fetchData request error', () => { return fetchData() .catch(e => { expect(e).toBe('error') }) });
jest 显示pass, 但这个error 的测试并无执行,由于 fetchData 返回数据了,没有机会执行catch error。按理说,这种状况要显示fail,表示没有执行到。怎么办,官网建议使用expect.assertions(1); 在测试代码以前,添加expect.assertions(1);
test('should return err when fetchData request error', () => { expect.assertions(1); // 测试代码以前添加 return fetchData() .catch(e => { expect(e).toBe('error') }) });
这时jest 显示fail 了。expect.assertions(1); 就是明确告诉jest, 在执行这个测试用例的时候,必定要作一次断言。后面的数字是几,就代表在一个test中,必定要作几回断言,若是没有执行catch,也就没有执行断言,和这里的1,要作一次断言不符,测试失败,也就达到了测试的目的。
对于promise的测试,还有一个简单的方法,由于promise 只有两种状况,一个是fullfill, 一个是reject,expect() 方法返回的对象提供了resolves 和rejects 属性,返回的就是resolve和reject的值,能够直接调用toEqual等匹配器。看一下代码就知道了
test('should return data when fetchData request success', () => {
return expect(fetchData()).resolves.toMatch(/userId/); // 直接在expect函数中调用 fetchData 函数 }); test('should return err when fetchData request error', () => { return expect(fetchData()).rejects.toBe('error'); });
除了使用then的链式调用,还能够用async/await 对promise 进行测试,由于 await后面的表达式就是promise. 这时test的函数参数以前要加上async 关键字了。
test('should return data when fetchData request success', async () => { let expectResult = { "userId": 1, "id": 1, "title": "delectus aut autem", "completed": false }; let data = await fetchData(); expect(JSON.parse(data)).toBe(expectResult); // 直接在expect函数中调用 fetchData 函数 }); test('should return err when fetchData request error', async () => { expect.assertions(1); try { await fetchData(); } catch (e) { expect(e).toBe('error'); } });
固然,也能够把async/await 与resolves 和rejects 相结合,
test('should return data when fetchData request success', async () => { await expect(fetchData()).resolves.toMatch(/userId/); // 直接在expect函数中调用 fetchData 函数 }); test('should return err when fetchData request error', async () => { await expect(fetchData()).rejects.toBe('error'); });
Mock 函数
有时你会发现,进行单元测试时,要测试的内容依赖其余内容,好比上面的异步请求,依赖网络,极可能形成测试达不到效果。 能不能把依赖变成可控的内容?这就用到Mock。Mock就是把依赖替换成咱们可控的内容,实现测试的内容和它的依赖项隔离。那怎么才能实现mock呢?使用Mock 函数。在jest中,当咱们谈论Mock的时候,其实谈论的就是使用Mock 函数代替依赖。Mock函数就是一个虚拟的或假的函数,因此对它来讲,最重要的就是实现依赖的所有功能,从而起到替换的做用。一般,mock函数会提供如下三个功能,来实现替换
函数的调用捕获,设置函数返回值,改变原函数的实现。
怎样建立Mock 函数呢?在jest 建立一个Mock 函数最简单的方法就是调用jest.fn() 方法。
const mockFunc = jest.fn();
函数的调用捕获指的是这个函数有没有被调用,调用的参数是什么,返回值是什么,一般用于测试回调函数,mock 真实的回调函数。就像官网举的forEach函数,它接受一个回调函数,每一个调用者都会传递不一样的回调函数过来,咱们事先并不知道回调函数,再者咱们测试forEach 的重点是,该函数是否是把数组中的每一项都传递给回调函数了,因此只要是一个函数就能够了,但该函数必须把调用的信息都保存下来,这就是Mock 函数的调用捕获,为此mock 函数还有一个mock 属性。在func.test.js 中,声明forEach, 而后写一个test测试,测试中就使用jest.fn() 生成的mock 函数来mock 真实的回调函数。
const forEach = (array, callback) => { for (let index = 0; index < array.length; index++) { const element = array[index]; callback(element); } } test('should call callback when forEach', () => { const mockFun = jest.fn(); // mock 函数 const array = [1, 2]; forEach(array, mockFun); // 用mock函数代替真实的回调函数 console.log(mockFun.mock); expect(mockFun.mock.calls.length).toBe(2) })
也打印出来mock函数mockFun的mock 属性
calls 保存的就是调用状态,results保存的就是返回值。calls 是一个数组,每一次的调用都组成数组的一个元素,在这里调用了两次,就有两个元素。每个元素又是一个数组,它则表示的是函数调用时的参数,由于每次的调用都传递了一个参数给函数,因此数组只有一项。若是有多个参数,数组就有多项,按照函数中的参数列表依次排列。这时候,就能够作断言,函数调用了几回,就判断calls.length. expect(mockFun.mock.calls.length).toBe(2) 就是断言函数是否是调用了两次。expcet(mockFun.mock.calls[0][0]) .toBe(1)就是断言第一次调用的时候传递的参数是否是1. 可能以为麻烦了, 的确有点麻烦了,幸亏,jest 对函数的mock参数进行了简单的封装,提供了简单的匹配器, toHaveBeenCalled(), toHaveBeenCalledTimes() ,使用起来有点方便了。你可能见过toBeCalled(), 其实,它和toHaveBeenCalled() 功能是如出一辙的,使用哪一个都行。
函数返回值。有的时候,你不想调用函数,直接获取到函数的返回值就能够了,好比异步函数, 以fetchData 为例,它直接返回一个promise 就行了,根本没有必要请求服务器。mock函数有mockReturnValue(), 它的参数就是返回值。不过它不能返回promise. 可使用mockResolvedValue直接返回promise的值. 对fetchData 进行mock, 而后设置它的mockResolvedValue()
const fetchData= jest.fn(); // fetchData.mockReturnValue("bar"); fetchData.mockResolvedValue({name:'sam'});
当咱们调用fetchData函数, 它就会返回{name: 'sam'}. 但这时又会发现另一个问题,fetchData 是从外部组件引入来的,没法在func.test.js 中直接mock. 咱们要先引入fetchData,测试fetchData 的时候,就能够这么写。引入fetchData, 而后让fetchData = jest.fn() 进行mock , 而后使用mockResolvedValue ()设置返回值, fetchData.mockResolvedValue ({name: 'sam'}), fetchData测试以下
引入fetchData并mock, 又叫mock module, mock 一个模块。 固然这也只是一种实现方式, 引入一个模块,而后对这个模块暴露出来函数依次进行mock, 当咱们测试的时候,调用模块暴露的函数就变成了调用mock函数,这也至关于mock了整个模块。可是若是一个模块暴露出不少函数,那么引入并mock的方式,就有点麻烦了。好比func.js 再暴露出三个简单 的方法。
const request = require('request'); exports.fetchData = function fetchData() { return new Promise((resolve, reject) => { request('https://jsonplaceholder.typicode.com/todos/1', function (error, response, body) { resolve(body); }); }) } exports.add = (a, b) => a + b; exports.subtract = (a, b) => a -b; exports.multiply = (a, b) => a * b;
引入并mock 的方式就变成了
let func = require('./func'); func.add = jest.fn(); func.subtract = jest.fn(); func.fetchData = jest.fu(); func.multiply = jest.fu();
有点麻烦,不过jest 提供了一个mock()方法,第一个参数是要mock的模块,能够自动mock这个模块。自动mock这个模块是什么意思呢?就是把模块暴露出来的方法,所有自动转换成mock函数jest.fn().(automatically set all exports of a module to the Mock Function). jest.mock('./func'), 就至关于把func.js 变成了
exports.fetchData = jest.fn();
exports.add = jest.fn(); exports.subtract = jest.fn(); exports.multiply = jest.fn();
不用每一个函数都单独mock,方便多了。当咱们再require('./func.js')的时候,就require 这个mock 函数了, 测试函数就变成了以下内容,不过要注意,先mock 模块,再require引入。
改变函数实现。 有时你不想使用默认的mock函数jest.fn(),尤为是测试回调函数的时候,你想提供回调函数实现,好比上面的forEach, 确实写一个真实的回调函数进行测试,内心更有底一点。mock 函数实现也有两种方法,jest.fn() 能够接受一个参数,这个参数就能够是一个函数实现。forEach 中的mock 函数就能够成
const mockFun = jest.fn(x => x + 1);
再调mockFun的时候, 实际上调的是x => x + 1; 函数。forEach 的测试修改一个
test('should call callback when forEach', () => { const mockFun = jest.fn(x => x + 1); const array = [1, 2]; forEach(array, mockFun); // 用mock函数代替真实的回调函数 console.log(mockFun.mock); expect(mockFun.mock.calls.length).toBe(2) })
能够看到有返回值了,测试正是调用了x =>x +1 函数实现
可是当咱们使用jest.mock() 来mock一个模块的时候,jest 已经把全部的方法自动mock成jest.fn(),没法给它传参,也就没法提供实现了。这就要用到第二种mock实现方法了,mock 函数提供了一个方法mockImplementation(), 它的参数也是一个函数实现,使用mockImplementation() 来mock fetchData,让它返回{name: 'sam'}
fetchData的整个测试
jest.mock('./func.js'); let fetchData = require('./func').fetchData; test('should return data when fetchData request success', () => { fetchData.mockImplementation(() => { return Promise.resolve({name: 'sam'}) }) return fetchData().then(res => { expect(res).toEqual({name: 'sam'}) }) })
以上jest.mock() 是mock 本身的module, 第三方模块好比request, 要怎么mock啊? 方法是同样,就是mock 的第一个参数要改一下,直接写要mock 的第三方模块名,它会从node modules 里面去找,而后自动mock. jest.mock('request'), request 模块暴露出来的就是jest.fn(). 若是不肯定jest.mock 第三方模块的时候,发生了什么,咱们能够先mock, 再require, 最后console.log 一下,仍是拿request 为例,按照三步走,代码以下,
jest.mock('request'); const request = require('request'); console.log(request);
能够看到不光整个模块被mock了,就连里面的方法也被mock了。这时再使用request, 就是使用的mock的 request了,mock 函数的全部用法都是可使用了,好比按照request 真实的使用方法,提供一个实现。如今就能够换一种思路来mock fetchData,因为fetchData调用request,咱们mock request, fetchData就不用mock了。在test 文件mock request 并提供实现,这时fetchData调用的就是mock的request了。
jest.mock('request'); const request = require('request'); request.mockImplementation((url, callback) => { callback(null, 'ok', {name: 'sam'}) }) const fetchData = require('./func').fetchData; test('should return data when fetchData request success', () => { return fetchData().then(res => { expect(res).toEqual({name: 'sam'}) }) })
对于这种简单的mock, jest.mock() 还提供了第二种写法,它的第二个参数是一个函数,返回一个mock 实现
jest.mock('request', () => { return (url, callback) => { callback(null, 'ok', {name: 'sam'}) } }); const fetchData = require('./func').fetchData; test('should return data when fetchData request success', () => { return fetchData().then(res => { expect(res).toEqual({name: 'sam'}) }) })
还有一种mock实现的方式,jest.spyOn(), 它接受两个参数,一个是对象,一个是对象上的某一个方法,返回一个mock函数,使用jest.spyOn() mock add方法,
使用spyOn 进行mock的好处是在同一个test 下,它能够restore, 恢复到之前默认mock的状态。这样就不用写beforeEach 和aftereEach 函数了。
const math = require('./func');
test("calls math.add", () => { const addMock = jest.spyOn(math, "add"); // override the implementation addMock.mockImplementation(() => "mock"); expect(addMock(1, 2)).toEqual("mock"); // restore the original implementation addMock.mockRestore(); expect(addMock(1, 2)).toBeUndefined(); });
固然,spyOn 还有另一个功能,就是监听函数,有时咱们并不想mock 函数,改变函数的实现,只想监听一下它有没有被调用。
const math = require('./func'); test('should call add', () => { function callMath(a, b) { return math.add(a + b); } const addMock = jest.spyOn(math, 'add'); callMath(1, 2); expect(addMock).toBeCalled(); // toBeCalled, 就是函数有没有被调用。 })
钩子函数
既然上面提了一个beforeAll, beforeEach, 就简单提一下Jest 的钩子函数,它的做用相对简单一点,就是作测试前的准备工做或测试后的清理工做。看名字也能知道它们的做用,beforeAll, 在全部测试以前作什么,beforeEach 在每个测试以前作什么。确实会有这样的需求,好比每次测试这前都要把值恢复到初始状态。我作过这样的一个测试
if(window.unicode || window.local || window.isEnable) { window.history.pushState('', '', '/uat=true') }
要作三种状况的测试,因此每个测试以前beforeEach, 我都把值设为了false
beforeEach(() => { window.unicode = false; window.local = false; window.isEnable = false })
要注意的是,这里的beforeEach, beforeAll 等,都是根据describe 来的,describe 表示一组测试,若是没有describe,那整个test文件就是一个describe.
describe('method called', () => { beforeEach(() => { window.unicode = false; window.local = false; window.isEnable = false }) describe('another beforeEach', () => { beforeEach(() => { window.unicode = false; window.local = false; window.isEnable = false }) }) })
但有时,在作初始化的时候,并无使用beforeEach, 但也没有什么问题,确实如此,但当describe 嵌套太多的时候,有可能就会出问题,使用console.log 输出一下,看一下执行顺序,就知道了。
describe('method called', () => { console.log("before each outer outer") beforeEach(() => { console.log('before each outer inner') window.unicode = false; window.local = false; window.isEnable = false }) describe('another beforeEach', () => { console.log('before each inner outer'); beforeEach(() => { console.log('beforeEach inner inner'); window.unicode = false; window.local = false; window.isEnable = false }) }) })
先输出before each outer outer, 再输出了before each inner outer, 能够看到,它把describe 下面全部的没有在钩子函数里面的语句先执行了,而后再执行钩子函数,而不是按照书写的顺序进行执行,必定要注意,最好仍是把全部的初始化工做放到钩子函数中.
Jest 在进行单元测试的时候,还能够生成测试代码覆盖率的报告,只要在run jest 的时候,提供一个参数coverage。按q 退出watch模式,输入npx jest --coverage, 能够看到
同时在根目录下,生成了一个测试coverage 目录,在lcov-report下, 有一个index.html 文件,打开,能够看到有测试了哪些文件或目录,点击目录,能够看到具体的文件,打开测试文件之后,在每一行前面会标有1x, 6x 等等,这表示这行代码执行了多小次。在代码内容上,它还有 一些标识, 好比 黑色的方块I,E, 还有黄色的标识,这都表示这个branch 没有测试,标红的代码则是直接没有测试到,须要咱们去覆盖。
Jest的基本内容说的差很少,最后再说一个babel 配置。因为Jest 默认是commonJs 规范,而咱们平时用的最多的确是ES module, import 和export。 这就须要在进行单元测试以前进行转化,ES6 语法的转化,确定是使用babel。安装babel, npm i @babel/core @babel/preset-env --save-dev 并在根目录下配置babel.config.js, babel-jest 不用安装了,安装jest的时候,已经自动安装了。
module.exports = { presets: [ [ '@babel/preset-env', { targets: { node: 'current', }, }, ], ], };
这时Jest 在进行单元测试的时候,就会自动转化node 不认识的语法。