团队开发小程序有一段时间了。随着功能的开发越来越多,我们测试同学回归的任务越来越重,所以我们决定用自动化测试来缓解一些回归测试的压力,也可以作为我们日常无障碍检查的工具,让我们进入正题。1.方案的确定方案主要是根据我们覆盖小程序的以下需求来覆盖web-view测试框架原生页面和web-view页面通信输出测试结果(excel,使用html在线展示)错误信息收集1.1覆盖面小小程序上能使用程序原生功能的程序并不多。经过团队的讨论,我们最终决定使用官方的miniprogram-automator。可以查看官方文档。这是一个基于nodejs的模块,所以对于我们js程序员来说,更容易上手。在此感谢测试同学的支持。她被迫使用js编写自动化测试。再次感谢。查看官方文档后,发现这个miniprogram-automator其实就是一个阉割过的Puppeteer,用过puppeteer的同学应该不会陌生。1.2覆盖web-view功能对于web-view的自动化,我们选择puppeteer(也可以查看中文api文档),它是一个基于nodejs的模块,它提供的API和功能可以满足我们的需求,在同时,微软推出的playwright也是一个不错的选择,但由于泰信需要重新了解api再尝试,所以暂时没有采用,希望以后有机会尝试一下未来。具体来说,我们使用nodejs模块puppeteer-core,因为我们不想在安装项目依赖的时候额外安装浏览器。此过程需要太多时间,并且存在安装不成功的风险。所以我们直接安装puppeteer-core,然后通过配置使用安装好的浏览器。具体和puppeteer的区别可以查看puppeteer-vs-puppeteer-core。1.3测试框架可供选择的测试框架还是挺多的,jest、mocka、jasmine等都不错。鉴于我们对框架的熟悉程度,我们选择了jest,比较简单,文档也很好。1.4输出测试结果我们期望测试结果的输出方式是excel和在线html预览,所以需要使用jest的自定义reporter配置项注册hook事件,测试完成后才能收到测试结果。具体配置内容可以查看官网jestcustomreporter1.5错误信息收集我们在收集测试结果的时候,还是需要关注测试用例中错误的具体信息。这部分测试报错信息是Jest返回的,但是返回的格式是ansi,显示不友好,所以需要引入另外一个模块ansi-to-html,可以方便的把ansi转成html,非常方便的。完美的。1.6native和web-view的通信鉴于我们web-view的访问地址需要由native页面生成,所以为了保证web-view可以直接使用这些地址,我们使用了一个临时文本文件存放这些地址信息,当puppeteer加载这些页面时,我们会在临时文本文件中查找。1.7方案总结经过以上步骤,我们的自动化测试项目结构如下:2.方案实现2.1配置小程序开发工具使用小程序官方提供的miniprogram-automator进行自动化测试,需要完成以下配置配置小程序开发工具的cli目录。该目录一般在小程序开发工具的安装目录下,路径地址分隔符需要使用'/',无论window还是mac,如'path/to/cli'。小程序原代码地址在windows下配置如下:{projectPath:'D:/project/code/product/mini-app/dist',cliPath:'F:/ssdprograms/微信web开发者工具/cli.bat'}Mac配置示例如下macOS/cli"}打开小程序服务端口打开端口配置2.2配置puppeteer因为我们使用的是puppeteer-core,所以需要为其额外指定一个浏览工具,因为我本地安装的是chrome,所以我配置了chrome浏览器的exe地址,但是就官方的说法,任何带有dev-tools的浏览器都可以作为它的运行浏览器,包括firefxo和edge。以下是我们项目的puppeteer配置内容,供参考。puppeteerCfg:{browserConfig:{executablePath:'C:\\ProgramFiles(x86)\\Google\\Chrome\\Application\\chrome.exe',headless:false,ignoreHTTPSErrors:true,devtools:true,defaultViewport:{宽度:1440,height:900,},args:['--no-sandbox','--disable-setuid-sandbox']},pageConfig:{waitUntil:'networkidle0',timeout:0},mockDevice:'iPhone6'}2.3配置Jest对于Jest的配置,除了常见的,我们还需要配置上述1.4输出测试结果的自定义测试结果的采集脚本。配置jes.config.js文件jest-repoerter.js文件的reporters项经过上面的配置,我们可以在每次测试后得到测试结果,但是我们仍然需要修改结果内容。对于输出excel文件的需求,我们使用exceljs,对于输出在线查看的需求,我们直接通过web-scoket或者sse(server-sentevents)窗口将测试结果发送到相应的浏览器。下面附上我们项目中使用的Jest配置文件jest.config.js,供大家参考。module.exports={moduleFileExtensions:['js','html','json'],transform:{'^.+\\.js$':'babel-jest'},moduleDirectories:['node_modules'],moduleFileExtensions:['js','json','html','scss','css'],moduleNameMapper:{'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|less|css)$':'/test/mocks/mock-file.js'},testMatch:['/test/**/*.spec.js'],globals:{'__DEV__':true,'__ENV__':'TEST'},globalSetup:'/scripts/jest-global-setup.js',globalTeardown:'/scripts/jest-global-teardown.js',reporters:["default","/scripts/jest-report.js"]}2.4编写测试代码对于编写测试代码,我们使用es6,所以需要在上面的Jest配置中设置transform配置项,接下来我们会罗列如何基于puppeteer-core和miniprogram-automator编写自动化代码,相信大家就明白了。使用miniprogram-automator的参考例子这个用来测试首页登录按钮,点击后跳转到登录页面constautomator=require('miniprogram-automator')constwaitTime=require('../scripts/util-wait-time')const{wsEndpoint}=require('../config')const{loadedLoginPage}=require('./test-login-models')jest.setTimeout(120*1000)//设置笑话完成当前文件下面所有测试用例所需的时间describe('UserLogin',()=>{letminiProgram=nullletpage=nullbeforeAll(async()=>{miniProgram=awaitautomator.connect({wsEndpoint})page=awaitminiProgram.currentPage()//默认是小程序的第一页awaitpage.waitFor(async()=>{constlocaNode=awaitpage.$$('.search-name')returnlocaNode.length>0})})it('点击首页广告后应该打开登录页面',async()=>{constentryNode=awaitpage.$('.ad-banner-image')awaitentryNode.tap()awaitwaitTime(5000)//让小程序完全完成页面的渲染,这里有点尴尬,需要给足够的时间,否则跳转后获取不到页面const{loginButton}=等待loadedLoginPage(miniProgram)expect(loginButton.length).toBeGreaterThan(0)})afterAll(async()=>{awaitmockLocation.doRestore({miniProgram})等待miniProgram.close()page=nullminiProgram=nullawaitwaitTime(3000)})})使用puppeteer-core的参考实例配置文件config.js{puppeteerCfg:{browserConfig:{executablePath:'C:\\ProgramFiles(x86)\\Google\\Chrome\\Application\\chrome.exe',headless:false,ignoreHTTPSErrors:true,devtools:true,defaultViewport:{width:1440,height:900,},args:['--no-sandbox','--disable-setuid-sandbox']},pageConfig:{//waitUntil:'networkidle2',waitUntil:'networkidle0',//waitUntil:'load',timeout:0},mockDevice:'iPhone6'}}对puppeteer操作的封装puppeteer-opts.jsconstpuppeteer=要求('puppeteer-core')const{puppeteerCfg}=require('../config')const{browserConfig,pageConfig,mockDevice}=puppeteerCfgconstallDevices=puppeteer['devices']letwsEnd=nullletbrowser=nullasyncfunctioncreateBrowser(){if(!wsEnd){browser=awaitpuppeteer.launch(browserConfig)wsEnd=browser.wsEndpoint()}else{浏览器=awaitpuppeteer.connect({browserWSEndpoint:wsEnd})}//global.browser=浏览器返回browser}asyncfunctioncreatePage(passedBrowser,linkUrl,customize){constphone=allDevices[mockDevice||'iPhone6']constpages=awaitpassedBrowser.pages()constpage=pages[0]//constpage=awaitpassedBrowser.newPage()awaitpage.emulate(phone)if(customize&&typeofcustomize==='function'){awaitpage.goto(linkUrl)awaitcustomize(page)}else{awaitpage.goto(linkUrl,customize||pageConfig)}returnpage}asyncfunctioncloseBrowser(){if(browser){awaitbrowser.close()browser=null}if(wsEnd){wsEnd=null}}module.exports={createBrowser,createPage,closeBrowser}元测试文件test.jsconst{createBrowser,createPage}=require('../scripts/puppeteer-opts')const{getStringFromTmpFile}=require('../scripts/tmp-file-opts')constwaitTime=require('../scripts/util-wait-time')const{customInfo}=require('../config')const{waitSSOcall}=require('./test-aa-models')letlinkUrl=nullletbrowser=nullletpage=nullconstinputInfo=customInfojest.setTimeout(300*1000)describe('PuppeteerSample',()=>{beforeAll(async()=>{constotaDeepLink=getStringFromTmpFile('otaDeepLink')linkUrl=otaDeepLinkconsole.log('**来自缓存的链接**',linkUrl)browser=awaitcreateBrowser()page=awaitcreatePage(browser,linkUrl,{waitUntil:'domcontentloaded',timeout:5*60*1000})awaitwaitSSOcall(page,'/um/v2/users/')})it('应该成功打开搜索结果页面',async()=>{constlistNodes=awaitpage.$$('a[class^=Button__ButtonContainer]')console.log('**searchlistlength**',listNodes.length)expect(listNodes.length).toBeGreaterThanOrEqual(1)})afterAll(async()=>{awaitbrowser.close()page=nullbrowser=null})})2.5执行测试用例我们设置Jest配置文件testMatch:['/test/**/*.spec.js']让Jest去特定目录收集.后缀名为spec的js文件中包含的所有测试用例在收集和执行的顺序上本质上是不需要人为干预的,但是如果我们对执行的顺序有要求,就需要使用额外的文件来控制执行顺序,如下截图所示,index.spec.js实现了依次执行两个测试用例文件的控件。3.显示结果。excel结果显示如下。html的结果显示如下。错误信息显示如下。4.注意事项所有授权都需要手动触发。元素需要在样式的前端加上组件名称作为前缀,并且不能被覆盖。在使用开发工具作为自动化测试终端之前,需要扫描并登录web-view。