本文转载自微信公众号《前端GoGoGo》,作者Joel。转载本文请联系前端GoGoGo公众号。编写测试可以减少错误并提高代码质量。同时重构测试覆盖率高的代码,不用担心改变之前的功能。前端测试可以分为3类:单元测试、集成测试和UI测试。单元测试是对软件最小单元的测试。例如:一个函数,一个组件。集成测试,也称为装配测试或联合测试。在单元测试的基础上,将所有模块按设计要求组装成子系统或系统进行测试。UI测试就是测试UI的渲染效果和交互。本文主要介绍使用Jest[1]编写单元测试。Jest是一个优雅简洁的JavaScript测试框架。下面是用Jest写的单元测试:importsumfrom'./sum';test('sum',()=>{expect(sum(1,2)).toBe(3);expect(sum(-1,-2)).toBe(-3);});sum是要测试的函数。test(...)包装要测试的功能。expect(...)是一个断言:期望sum(1,2)的值为3,如果sum(1,2)的值不为3则测试失败。Jest主要由3块组成:安装。跑步。笑话API。1安装Jest依赖于Node.js[2]。安装Node后,初始化node项目:npminit-yinstallJestnpminstall--save-devjestsupportsBabel需要支持ES6、ES7等语法,然后安装Babel相关依赖:yarnadd--devbabel-jest@babel/core@babel/preset-env在项目根目录下创建一个Babel配置文件:babel.config.js来配置兼容当前Node版本的Babel:module.exports={presets:[['@babel/preset-env',{targets:{节点:'当前'}}]],};支持TypeScriptyarnadd--dev@babel/preset-typescript在项目根目录下创建TypeScript配置文件:tsconfig.json。内容类似:{"compilerOptions":{"module":"commonjs","noImplicitAny":true,"removeComments":true,"preserveConstEnums":true,"sourceMap":true,"esModuleInterop":true},"include":["src/**/*"],"exclude":["node_modules"]}2要运行项目中的所有测试用例,需要在package.json中添加如下内容:{"scripts":{"test":"jest"}}测试用例应该写在一个单独的文件中,而不是在被测试的文件中。测试用例的文件名需要有.spec.[js|ts]或.test.[js|ts]。例如,如果一个文件名为sum.js,要对其进行测试,一般在该文件的同一目录下创建一个测试用例文件sum.spec.js。执行npmruntest运行项目,将运行所有测试用例。运行具体的测试用例文件,在项目根目录执行yarnjest测试用例文件路径或者npmruntest测试用例文件路径。运行一个具体的测试用例,只需将测试用例的test(...)改为test.only(...),然后运行测试用例文件即可。同样,要运行一组用例,请将describe(...)更改为describe.only(...)。获取测试覆盖率测试覆盖率(testcoverage)衡量功能代码被测试用例覆盖的比例。对代码质量要求高的项目,都会要求测试覆盖率达到90%以上。获取测试覆盖率的命令:jest--coverage3JestAPIassertionsAPI在编写测试时,我们总是会做出一些假设,而断言就是用来在代码中捕获这些假设的。例如:expect(2+2).toBe(4)Jest支持的主要断言API如下:所有断言API都可以在这里找到:[3]。Jest场景测试异步代码一般有三种处理异步的方式:callback、Promise和Async/Await。回调业务代码:functionfetchNameCallback(cb:(name:string)=>void){setTimeout(()=>{cb('Joel');},1000)}用例代码如下:test('async:callback',done=>{fetchNameCallback(name=>{expect(name).toBe('Joel');done();//通知Jest回调结束});});注意:异步回调结束后,需要调用参数done。这样,Jest就被通知回调结束了。Promise业务代码:functionfetchName(throwError?:boolean){returnnewPromise((resolve,reject)=>{setTimeout(()=>{if(!throwError){resolve('Joel')}else{reject('errorhappened')}},1000)});}用例代码如下:test('async:Promise',()=>{fetchName().then(name=>{expect(name).toBe('Joel');});//异常处理fetchName(true).catch(e=>{expect(e).toMatch('error');});});Async/Await业务代码与上面的Promise相同。用例代码如下:test.only('Async/Await',async()=>{constname=awaitfetchName();expect(name).toBe('Joel');//异常处理try{fetchName(true);}catch(e){expect(e).toMatch('error');}});测试前的准备工作和测试后的整理工作在写测试的时候,可能需要做一些测试前的准备工作。运行测试后,需要做一些内务处理。用Jest这样写:在每个用例前后执行//beforeEach(()=>{});//afterEach(()=>{});在所有用例执行之前和之后执行beforeAll(()=>{});afterAll(()=>{});只会被执行一次。Mock外部依赖当我们测试模块功能时,如果模块的外部依赖出现问题,同样会导致测试失败。为了避免这个问题,Mockexternaldependencies:replaceexternaldependencieswithanerror-freeimplementation。Mock第三方包部分API以Mockaxios[4]为例,业务代码:axiosFetchUser=()=>{returnaxios.get('/user');}测试代码:test('fetchuser',()=>{axios.get.mockImplementation(url=>{if(/^\/user$/.test(url)){returnPromise.resolve({name:'Joel'})}returnPromise.resolve('其他')})axiosFetchUser().then(({name})=>{expect(name).toBe('Joel');})});其中axios.get.mockImplementation(...)模拟axios.get完成。Mock部分文件我们有一个工具函数文件utils.ts,内容如下:constguid=()=>...;exportdefaultguid;exportconstgetYear=()=>...;exportconstgetMonth=()=>。..;我们只mock了上面文件中的guid和getYear,其他部分保持原样。像这样写:jest.mock('./util',()=>{constooriginalModule=jest.requireActual('./util');return{__esModule:true,...originalModule,default:()=>'abc',//mockguidgetYear:()=>2021,};});模拟完整的第三方包和文件模拟完整的第三方包和文件只需要2个步骤。在__mocks__/下创建Mock文件。告诉Jest使用Mock的实现:jest.mock('./moduleName')或jest.mock('module_name')。详细介绍见:这里[5]。终于行动起来,开始练习写单元测试了~参考文献[1]Jest:https://jestjs.io/[2]Node.js:https://nodejs.org/en/[3]这里:https://jestjs.io/zh-Hans/docs/expect[4]axios:https://axios-http.com/[5]这里:https://jestjs.io/zh-Hans/docs/manual-mocks
