文章主旨介绍概念和思考过程,不提供代码(具体代码编写方法请参考jest官网)延伸:在信息爆炸的时代,各种资源丰富。但官网不重复相同信息,造成额外的精神负担。大脑只是一个搜索引擎。它知道在哪里可以找到资源。不负责记录具体做法,节省记忆。测试的几个名称视觉测试:【测试工具】前端视觉比较多所以视觉测试成本高,普及度不高,但优点是可以测试样式信息。单元测试:【测试目标】最小粒度测试,针对单个功能或功能,适用于函数库、基础组件库等TDD(TestDrivenDevelopment测试驱动开发):【方法论】先写测试用例(提出expectedvalue),然后写出具体的实现方法和函数,用于单元测试BDD(BehaviorDrivenDevelopment):【方法论】基于集成测试,本文主要介绍jest(笑话)单元测试库jest的原理和局限性单元测试。知道它的功能边界,能做什么,不能做什么,了解jest在node端运行的能力范围,底层实现库是jsdom,用node模拟一个dom环境,模拟的范围是限于dom层级和操作[domOperation]只模拟了dom的大部分通用功能,一些特定的domapi不支持,比如canvas,视频媒体功能api。如果要测试canvas、视频媒体API,需要安装相应的扩展库,可以理解为在节点端实现浏览器的功能,比如图片生成、音视频播放等canvas扩展,和video相关的扩展暂时没找到[cssstyle]严格来说没有css样式模拟功能,css在jsdomString中只是被当做一个纯dom属性字符,和id、classstring没有区别不支持继承,每个dom都是一个独立的个体,不支持样式继承。只支持内联样式,无法识别vue中的样式。这不是很有用。解析外部链接样式的示例在这里。有解决办法,但是官方没有合并非内联样式测试。您需要使用可视化测试库。单元测试需要覆盖那些场景?直接运行单元测试可以发现代码变更,但是如何防止开发人员忘记运行单元测试呢?通过添加cicd进程解决。当提交合并请求申请时,单元测试被触发。如果操作失败,会自动拒绝合并请求,并执行node命令发送消息提醒gitlabci。新代码的相关配置将在文末介绍,运行旧的单元测试不会覆盖,如何提醒开发者覆盖这部分新的代码?通过配置测试覆盖的行数为100%来解决。如果没有达到目标,则测试将被视为失败,以避免遗漏新代码。如何解决无法覆盖的分支或功能?通过配置“ignoreremarks/istanbulignorenext/”,保持一个文件的100%覆盖率测试。如果有时间,也可以全局搜索这些ignore配置,将测试一一覆盖,起到标记的作用。coverageThreshold:{'./src/common/js/*.js':{branches:80,//代码逻辑分支覆盖的百分比functions:80,//函数覆盖的百分比lines:80,//代码行的百分比coveredstatements:-10//如果uncovered语句多于10个,jest会失败}},添加文件,是否缺少测试一般情况下,单元测试只会运行单元测试文件,新添加的代码文件没有相应的测试文件,会在漏测的情况下,通过collectCoverageFrom参数指定要覆盖的文件夹。当文件夹中的文件没有对应的测试用例时,将被视为覆盖率为0,作为新文件遗漏测试的提醒。//从那些文件夹中生成覆盖率信息,包括未设置测试用例的文件,解决缺少新文件的测试覆盖率问题collectCoverageFrom:['./src/common/js/*.{js,jsx}','./src/components/**/*.{js,vue}',],一些特殊场景下的功能(经验值),正常情况下运行没有问题,只会报错特殊情况下,比如简单的加法运算,如果放在小数点,会出现计算错误,0.1+0.2=0.30000000000000004这些特殊场景的覆盖范围只能由一线开发人员在实际工作中记录,需要时间积累.这就是程序员经验的价值,也是难得的。是的,一些值得继承的单元测试忽略了jestcollectscoverage的原则。底层使用istanbul库(istanbul:伊斯坦布尔,生产地毯,地毯用于覆盖)。以下忽略格式是istanbul库的函数。忽略这个文件,放在文件顶部/istanbulignorefile/忽略一个函数,一段分支逻辑或者一行代码,放在函数顶部/istanbulignorenext/忽略默认值函数参数functiongetWeekRange(/*istanbul忽略下一个*/date=newDate()){具体的忽略规则可以参考istanbulgithub介绍写测试用例的正确姿势。出发点是对功能的期望和定位,而不是代码。一开始应该先想好函数或者工具库需要发挥的作用。不要一开始就看代码,列出你所期望的,组件或功能的功能,并用文字写出来。这也是test('detectclickevent')中描述的函数,告知别人本次测试用例的用途,并编写相应修改不符合测试用例的代码,观察代码覆盖率,覆盖所有行代码。添加jest全局自定义函数。如果某个测试函数频繁出现,可以考虑对齐复用,写成预加载文件。在每个测试文件执行之前,加载文件比较麻烦,比如获取dom样式的原始代码,wrapper.element.style.height,而且元素还没有正式暴露,是一个内部变量。可以通过添加配置文件的方式编写样式全局方法,通过函数获取样式数据,并与类等方法保持一致:['./jest.setup.js'],//./jest.setup。jsimport{shallowMount}from'@vue/test-utils'//将通用函数样式挂载到全局包装器中并返回元素内容内联样式(因为jsdom只支持内联样式,不支持检测类中的样式),或内联样式函数的值addStylesFun(){//生成一个临时组件,获取vueWrapper和domWrapper实例,并挂载样式方法constvueWrapper=shallowMount({template:'
componentForCreateWrapper
'})constdomWrapper=vueWrapper.find('div')vueWrapper.__proto__.styles=function(styleName){返回样式名称?吨his.element.style[styleName]:this.element.style}domWrapper.__proto__.styles=function(styleName){返回styleName?this.element.style[styleName]:this.element.style}}addStylesFun()钩子函数类似vuerouter中的guard函数,钩子函数在进入前后执行,解决有状态函数的数据存储问题,避免需要重复写代码准备数据beforeAll和afterAll在执行每个测试用例的时候都写在单元测试文件的最外层,代表函数在文件执行前后执行一次,写在最外层testgroupdescribe,表示函数在testgroup执行前后各执行一次。BeforeEach、afterEach是在每个测试用例前后执行快速单元测试(test)的技术,跳过测试成功且源代码没有变化的用例,第一步不再多余。如果jest--watchAll测试文件发生变化,则只能在packagescript命令中加入自动执行测试。在npm命令执行不生效后,源代码更改或单元测试文件更改会触发第二步。按f(只执行错误的用例)。缺点是无法监控成功执行的单元测试的变化以及相应的源代码变化。(即之前已经成功的会被忽略,不管有没有新的变化或者是否有错误发生)源代码变化,或者单元测试文件变化,都会触发全局遍历的第三步,可以通过反复按f来切换,然后按o(只执行源代码发生变化的文件的测试用例)相当于开玩笑--watch只监听git中还没有提交到暂存区的文件。stash一旦提交,就不再触发,即使文件中有失败的测试用例也将被忽略。按a运行文件的所有测试用例,即a和o切换最底层是读取.git文件夹的内容来区分文件,所以根据git的存在,按w显示菜单.勾选watch的选项一般设置o和f使用,先o(忽略没改的文件,当我们改文件的时候会监听反复按f,只监控错误的用例)jest报告表示鼠标悬停在相应的图表上,会显示相应的提示。“5x”表示该语句在测试过程中被执行了5次。如果不输入条件,则“I”是测试用例。也就是说,没有if为真的测试用例。“E”为if条件为假时的测试用例,即测试用例中的if条件始终为真。你要写一个if条件为假的测试用例,也就是不输入if条件里的代码,这个E就会消失。模拟函数,不是模拟数据的函数只是模拟函数(Function,jest.fn()),不是像mockjs那样生成模拟数据的函数功能:检测函数执行了多少次,检测何时执行函数,this指向执行检测时的??入参,覆盖执行检测后的返回值。模拟第三方函数重写axios函数,避免实际发起接口,自定义具体返回值。jest.mock('axios');axios.get.mockResolvedValue(resp);没有魔法,没有私有适配,只是简单的函数重载。相当于axios.get=()=>resp重写了这个方法的终极方法,覆盖了整个第三方库,写了一个替代文件。使用import导入时,导入的替代文件也可以通过jest.requireActual('../foo-bar-baz')强制导入为真实文件,不要使用aliasfiletimer模拟覆盖setTimeout定时器,可以跳过指定的时长,缩短单元测试运行时间testsnapshotsnapshot,也就是数据拷贝,也就是检测“当前数据”和“旧数据拷贝”是否相同,类似JSON.stringify(),序列化记录数据应用场景限制配置文件的变化检测dom结构的比较,某个函数的变化是否影响dom结构一般在比较操作中使用大数据,以避免在单元测试文件中硬写数据。其他难处理的别名和等价方法是test的别名,两者等价于BeTruthy!==toBe(true)、toBeFalsy!==toBe(false),toBe(true)更严格,toBeTruthy被强制后为真booleanskip跳过一个测试用例,比注释更优雅describe.skip('Testcustominstructions',xxx)`test.skip('testcustominstruction',xxx)`开玩笑,内部使用Object.is进行比较。与===的区别在于,除了NaN、+0和-0外,其行为与三个等号相同,解决小数点浮点计算错误问题的运算符toBeCloseTo异步测试,通过.resolves/.rejects强制验证promise采取特定的分支test('thedataispeanutbutter',()=>{returnexpect(fetchData()).resolves.toBe('peanutbutter');});解决默认参数为newDate的覆盖问题test('当前月份,测试参数newDate默认值',()=>{//overridenewDatemock的值为2022/01/2317:13:03、解决默认参数为newDate不能被覆盖的问题constmockDate=newDate(1642938133000)constspyDate=jest.spyOn(global,'Date')//监控global.Date变量。mockImplementationOnce(()=>{spyDate.mockRestore()//mock第一次执行后需要立即淘汰,避免后续影响follow-upnewDatereturnmockDate})let[starTime,endTime]=getMonthRange()expect(starTime).toBe(1640966400000)//2022/01/0100:00:00expect(endTime).toBe(1643644799000)//2022/01/3123:59:59})等同于使用原生语法使用最新的语法beforeAll(()=>{jest.useFakeTimers('modern')jest.setSystemTime(newDate(1466424490000))//因为VueTestUtils中使用的jest是24.9版本,没有这个功能})afterEach(()=>{jest.restoreAllMocks()})匹配测试,并使用多批次ofdatatorunsametestcasedescribe.each([[1,1,2],//每一行代表运行一个测试用例[1,2,3],//每行中的参数是用于运行测试用例,前两个are参数,第三个是测试的期望值[2,1,3],])('.add(%i,%i)',//设置describe的标题,%i是可变参数ofprint(a,b,expected)=>{test(`returns${expected}`,()=>{期望(a+b).toBe(预期);});});gitlab-ci单元测试相关配置当发起合并请求时,触发ci执行单元测试当单元测试失败时,执行节点文件并发送飞书信息,飞书信息中包含合并请求的链接,可以点此链接快速定位单元测试作业并查看问题阶段:-merge-unit-test-merge-unit-test-fail-callback-other-test#合并时执行的jobstep-merge:stage:merge-unit-testrequested#使用的gitlabrunnertags:[front-end]#仅当有代码合并请求时才执行:[merge_requests]#排除特定分支的代码合并请求,即特定分支的代码时不执行作业被合并除了:变量:-$CI_MERGE_REQUEST_TARGET_BRANCH_NAME=="qa"#运行命令脚本:-npminstall--registry=https://registry.npm.taobao.org#安装依赖#2>&1标准错误指向standardoutput#Linuxtee命令用于从标准输入读取数据,将其内容输出为文件-npm运行测试2>&1|teeci-merge-unit-test.log#执行单元测试并将控制台输出的信息保存在ci-merge-unit-test.log文件中,以供后续分析-echo'merge-unit-test-finish'#定义需要传递给下一个作业的数据artifacts:when:on_failure#默认只有成功才会保存,可以通过这个标识符进行配置paths:#定义需要传递的数据File-ci-merge-unit-test.log#合并测试失败时执行的节点命令step-merge-unit-test-fail-callback:stage:merge-unit-test-fail-callback#当上一个作业执行失败时:on_failuretags:[front-end]only:[merge_requests]except:variables:-$CI_MERGE_REQUEST_TARGET_BRANCH_NAME=="qa"script:-nodeci-merge-unit-test-fail-callback.js$CI_PROJECT_NAME$CI_JOB_ID#执行节点脚本,通知飞书,并携带对应链接快速定位ci-merge-unit-test-fail-callback.js.jsconstfs=require('fs')constpath=require('path')consthttps=require('https')constprojectName=process.argv[2]//项目名constjobsId=process.argv[3]//执行的ci任务idconstlogsMainMsg=fs.readFileSync(path.join(__dirname,'ci-merge-unit-test.log')).toString().split('\n').filter(line=>line[line.length-1]!=='|'&&line.indexOf('PASS')!==0)//过滤不关心的信息.join('\n')constdata=JSON.stringify({msg_type:'post',content:{post:{zh_cn:{content:[[{tag:'a',text:'gitlabmergeunittest',href:`https://xxx/fe-team/${projectName}/-/jobs/${Number(jobsId)-1}`},{tag:'text',text:`运行失败\r\n${logsMainMsg}`}]]}}}})constreq=https.request({hostname:'open.feishu.cn',port:443,path:'/open-apis/bot/v2/hook/xxx',method:'POST',headers:{'Content-Type':'application/json'}},res=>{console.log(`statusCode:${res.statusCode}`)res.on('data',d=>process.stdout.write(d))})req.on('error',error=>console.error(error))req.write(data)req.end()感谢最近文章输出不足,东西太多了,懒得感谢网友们的关心和监督,被想念的感觉真好