前不久听说某公司有两个前端,天天窃听,为什么呢?只是测试MM人满为患,青玉哥哥出了bug。?(??????)??不过最后两个有点担心Σ(っ°Д°;)っ。测试MM提了几次紧急bug,鼓励他们修复bug。但是线上bug重现和排查比较麻烦,改了之后发现还没有修复,害得测试MM哭了。怎么做?前端Jun666某天发现E2E测试神器cypress后,偷偷修炼神功,改bug,越来越6了,测试MM每天笑着对他说,666,你真6,MM太喜欢了(???????)另一位前端君,555,每天面对堆积如山的bug叹息叹息,测试MM提新bug后都不理他。(???_??)?,我觉得有必要学习一下如何使用cypress进行E2E测试,提高代码质量。那么让我们来看看如何开始使用cypress测试框架。柏树三问——你是谁?cypress是一个开箱即用的E2E测试框架,它建立在类似mocha的API的基础上。与其他测试框架相比,它提供了一套属于自己的最佳实践解决方案,无需其他测试工具库和配置。方便简单却极其强大,可以使用webpack项目配置,还提供了强大的GUI绘图工具。它易于上手且易于使用。舒适度如何?(?????)cypressGUI测试使用真实浏览器,非GUI测试使用chrome-headless代替模拟测试,更真实地展示了实际环境下的测试过程和结果。赛普拉斯三问——你的优势是什么?Cypress有几个强大的功能:内置的GUI工具,你可以点击任何你想测试的东西,你也可以查看整个测试过程,如果你想的话,你可以录制屏幕(你可以把屏幕录像发送给Showit考MM了,她肯定说我哥真是了不起。普通人,我不告诉他(?乛?乛?)测试的每一步都有快照,可以通过GUI工具查看每个进程的页面状态,不是截图而是真实的页面DOM环境!内置数据mock和请求拦截机制,轻松还原线上数据和wepbakc配置导致的bug,无论测试文件或被测代码是否修改,实现自动重测跳过测试用例以避免重新测试测试文件中的所有用例:it.only('onlytestthis);it.skip('不要测试这个');cypress三问-如何安装yarnaddcypressornpminstallcypress安装完后./node_modules/.bin/cypressinstall安装cypress环境(包括GUI工具)并配置package.json:配置GUI和非GUI(terminal)运行cypress"scripts"的两种方式:{"cypress":"cypressrun","cypress-gui":"cypressopen",??配置完成后,运行yarncypress[-gui]或npmruncypress[-gui](括号表示可选)初始化cypress,__generatedefaultconfiguration和目录__cypress.json(与package.json同级目录):cypress提供了更灵活的配置,你可以根据自己的需要自定义行为。下面是我对一个项目的配置{"baseUrl":"http://localhost:8080",//本地开发服务地址(webpack-dev-server)"integrationFolder":"src",//自定义"src"作为测试文件的根目录,默认为"cypress/integration""testFiles":"**/*.cypress.spec.js",//自定义测试文件匹配规则,默认为"**/*.*",即所有文件"videoRecording":false,//关闭屏幕录制功能,如果开启屏幕录制功能,记得将"cypress/screenshots"目录添加到".gitignore"中,以防不小心添加屏幕录像gotogit"viewportHeight":800,//设置测试环境页面视图的高度"viewportWidth":1600//设置测试环境页面视图的宽度}cypress/plugins/index.js:cypress运行时环境配置,可以使用来配置webpack等,下面是配置webpack别名的例子。默认情况下,这里不需要配置。//参考官方示例地址https://github.com/cypress-io/cypress-example-recipes/blob/master/examples/preprocessors__typescript-webpack/cypress/plugins/index.jsconstwp=require("@cypress/webpack-preprocessor");constpath=require('path');functionresolve(dir){returnpath.join(__dirname,"../..",dir);}module.exports=on=>{constoptions={webpackOptions:{resolve:{别名:{"@":resolve("src"),cypress:resolve("cypress")}}}};on("file:preprocessor",wp(options));};万事俱备,一个简单的测试例子describe('测试页面包含某个元素',()=>{it('有云'前端哥哥好帅,前端姐姐好美"',()=>{cy.contains("前端哥哥好帅,前端姐姐好美");});it('必须有一个链接',()=>{cy.get('a').should('have.length',1);});it('没有类为abc的元素',()=>{cy.get('.abc').should('have.length',0);});});交互示例describe('starttogether',()=>{it('获取输入框,输入文字并回车',()=>{consttext='不存在';//输入api用法:https://docs.cypress.io/api/commands/type.html#Usagecy.get('input').type(`${text}{enter}`);});it('点击按钮',()=>{cy.get('按钮').click();});});networkrequestmockexampleTip1:cy.route的路径匹配比较严格,注意是否需要加通配符。比如cy.route('/api/search',[])不会拦截/api/search?keyword=abc,只会拦截/api/search。Tip2:注意cy.route的方法,默认是GET,cy.route('/api/posts')和cy.route('POST','/api/posts')是不同的。describe('Whattogive',()=>{beforeEach(()=>{cy.server();//一定要在cy.route之前调用cy.fixture('/posts/list.json')//我们在cypress/fixtures中创建mock数据。as('postsData');//给mock数据起别名,后面cy.route会使用cy.route('/api/posts','@postsData').as('getPostsRou??te');//为cy.wait使用的请求起别名})it('进入列表页面拦截列表请求接口',()=>{cy.wait('@getPostsRou??te');//等待拦截接口请求完成cy.get('.post').should('have.length',10);//必须有10条数据才能渲染到页面上});})实际场景示例:结合以上所有姿势,我们现在测试搜索页面的搜索和运行结果describe('测试搜索页面',()=>{//几个route路径变量constsearchRoutePath='/api/items/activities?query=*';constdeleteActivityRoutePath='/api/activities/*/items/batch?num_iids[]=*';constundoActivityRoutePath='/api/activities/*/items/undo';functionsearch(keyword){//将搜索行为和等待搜索返回进行封装cy.fixture('items/activities.json')//处理mock数据,只返回匹配搜索结构的数据.then(data=>data.filter(item=>item.title.indexOf(keyword)!==-1)).as('searchResult');cy.server();cy.route(searchRoutePath,'@searchResult').as('searchRoute');constinput=cy.get('输入');输入.clear();//清除输入框中的文本input.type(`${keyword}{enter}`);cy.wait('@searchRoute');}before(()=>{//在所有测试之前,访问搜索页面cy.visit('/activities/search');});it('当搜索结果为空时应该不显示数据提示',()=>{consttext='notexist';search(text);cy.contains(`Noresultsfoundfor${text}`);});it('清除成功后应从列表中删除活动',()=>{search('successful');cy.route('delete',deleteActivityRoutePath,{success:0,fail:0,waiting:0,}).as('deleteActivityResponse');//within是cy执行的上下文保留在dom节点'.activities-search'//默认执行cy是前面cy命令的结果作为上下文//比如"cy.get('a');cy.get('span')",cy将在上一个命令cy.get('.activities-search').within(()=>{constitems=cy.get('.result)找到的'a'标签中查找'span'-item');items.should('have.length',1);constapplyList=items.get('.apply-list');applyList.should('not.be.visible');//详细内容在每个数据项区域隐藏consttoggleBtn=items.get('.item-apply-count');toggleBtn.click();//点击显示详细内容区域applyList.should('be.visible');applyList.children().should('have.length',1);//详细内容区只有1条数据constcleanBtn=cy.contains('exit');cleanBtn.click();//在详细内容区点击“退出”按钮cy.wait('@deleteActivityResponse');//等待“退出”请求返回cy.get('.apply-list').should('be',null);//退出成功后,详细内容区数据为负1,为空});});});几个必读文档network-requests:https://docs.cypress.io/guide...assertions:https://docs.cypress.io/guide...食谱示例:https://docs.cypress.io/examp...代码完成代码提示:https://docs.cypress.io/guide...关于测试覆盖率在目前,cypress没有内置的测试覆盖率统计功能。github上有一个特殊的问题来跟踪这个。未来应该有一些临时解决方案。目前我比较倾向于使用chrome自带的来查看。在打开GUI的测试浏览器中打开devtools,切换到Sources,按cmd+shift+p(windows用户按ctrl+shift+p),输入coverage,选择refresh并统计代码执行覆盖率。然后,gethighforhigh(teasing)quality(measurement)quantity(test)code(M)code(M),gethigh。喜欢前端MM的可以手把手教一下(¬_¬)本文首发于本人公众号:枫叶。有兴趣的可以长按下方二维码关注。^v^
