微信小程序自动化测试的实践

团队开发小程序已经有一段时间了,随着开发的功能越来越多,我们的测试同学回归的任务也越发的重,所以我们决定用自动化测试来减轻一些回归测试的压力,同时也可以用来作为我们应用日常可访问性检查的一个工具,话不多说我们马上进入正题。
1. 方案确定 方案主要是围绕以下我们的几个需求

  1. 覆盖小程序
  2. 覆盖web-view
  3. 测试框架
  4. 原生页面与web-view页面的通讯
  5. 输出测试结果(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及尝试,所以暂时没有采用,以后希望有机会可以尝试一下。
具体来讲我们使用的是puppeteer-core这个nodejs模块,因为我们不太希望在安装项目依赖时候要额外安装一个浏览器,这个过程太花时间了,而且还有安装不成功的风险。所以我们直接安装puppeteer-core,然后使用配置使用已经安装好的浏览器即可,具体与puppeteer的差异,我们可以查看puppeteer-vs-puppeteer-core。
1.3 测试框架 测试框架可供选择的还是挺多的jest, mocka, jasmine等都是不错的,鉴于我们对框架的熟悉程度,我们选择了jest,相对简单并且文档也还不错。
1.4 输出测试结果 我们期望测试结果的输出方式是excel及在线的html预览,所以我们需要使jest的自定义reporter配置项来注册hook事件,在测试完成后可以接收到测试的结果。具体配置内容可以查看官网jest自定义reporter
1.5 原生与web-view之间的通讯 鉴于我们的web-view的访问地址是需要原生的页面产生的,所以为了能够保证web-view可以直接使用这些地址,我们使用了一个临时的文本文件来存储这些地址信息,在puppeteer在加载这些页面的时候,我们会在临时的文本文件里边查找。
1.6 方案总结 经过上述的多个步骤,最终我们的自动化测试项目架构如下:
微信小程序自动化测试的实践
文章图片

2. 方案实施 2.1 配置小程序开发工具 在使用小程序官方提供的miniprogram-automator进行自动化测试,我们需要完成以下几个配置
  1. 配置好小程序开发工具的cli目录,此目录一般是在小程开发工具的安装目录下,并且路径地址分格符需要使用 ‘/’无论window还是mac,如‘path/to/cli’
  2. 配置好小程序原码的地址
windows下配置例如下:
{ projectPath: 'D:/project/code/product/mini-app/dist', cliPath: 'F:/ssd programs/微信web开发者工具/cli.bat' }

mac下配置例如下
{ projectPath: "/Users/nb666/Desktop/me/project/product/code/mini-app/dist", cliPath: "/Applications/wechatwebdevtools.app/Contents/MacOS/cli" }

  1. 打开小程序服务端口
微信小程序自动化测试的实践
文章图片

打开端口配置
2.2 配置puppeteer 因为我们用的是puppeteer-core,所以我们需要额外指定一个浏览工具给它,因为我本机安装的是chrome,所以我配置的是chrome浏览器的exe地址,不过就官方说明,只要有dev-tools的浏览器都可以用作它的运行浏览器,包括firefxo跟edge。
以下是我们这边项目的puppeteer配置内容,供参考。
puppeteerCfg: { browserConfig: { executablePath: 'C:\\Program Files (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: 'networkidle0', timeout: 0 }, mockDevice: 'iPhone 6' }

2.3 配置Jest 对于Jest的配置,除了常用的之外,我们还需要配置上述1.4 输出测试结果的自定测试结果的收集脚本。
微信小程序自动化测试的实践
文章图片

配置jes.config.js文件的reporters项
微信小程序自动化测试的实践
文章图片

jest-repoerter.js文件
在上述的配置后,我们在每次完成测试后是可以拿到测试结果的,不过我们还要需要对结果内容进行修饰。对于要输出excel文件的需求,我们使用的是exceljs,而对于要输出在线查看的需求,我们将测试结果直接通过web-scoket或者sse(server-sent events)将结果下发给到对应的浏览器窗口。
下面附上我们项目中使用到的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-coreminiprogram-automator如何实现自动化代码的编写,相信大家就会明白了。
  1. 使用miniprogram-automator的参考实例
这个是用来测试首页的登录按钮点击后能跳转到登录页面
const automator = require('miniprogram-automator') const waitTime = require('../scripts/util-wait-time') const { wsEndpoint } = require('../config') const { loadedLoginPage } = require('./test-login-models')jest.setTimeout(120 * 1000) // 设置jest完成当前文件下面所有的test case所需要用的时间describe('User Login', () => { let miniProgram = null let page = nullbeforeAll(async () => { miniProgram = await automator.connect({ wsEndpoint })page = await miniProgram.currentPage() // 默认会是小程序的第一个页面await page.waitFor(async () => { const locaNode = await page.$$('.search-name') return locaNode.length > 0 }) })it('should open login page after click the entry ad. in home page', async () => { const enteryNode = await page.$('.ad-banner-image') await enteryNode.tap()await waitTime(5000) //让小程序充分完成页面的渲染,这里有点尴尬,需要给足够的时间,不然拿不到跳转后的页面const { loginButton } = await loadedLoginPage(miniProgram) expect(loginButton.length).toBeGreaterThan(0) })afterAll(async () => { await mockLocation.doRestore({ miniProgram })await miniProgram.close() page = null miniProgram = null await waitTime(3000) }) })

  1. 使用puppeteer-core的参考实例
配置文件config.js
{ puppeteerCfg: { browserConfig: { executablePath: 'C:\\Program Files (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: 'iPhone 6' } }

对puppeteer操作的封装 puppeteer-opts.js
const puppeteer = require('puppeteer-core') const { puppeteerCfg } = require('../config') const { browserConfig, pageConfig, mockDevice } = puppeteerCfg const allDevices = puppeteer['devices']let wsEnd = null let browser = nullasync function createBrowser() { if (!wsEnd) { browser = await puppeteer.launch(browserConfig) wsEnd = browser.wsEndpoint() } else { browser = await puppeteer.connect({ browserWSEndpoint: wsEnd }) }// global.browser = browserreturn browser }async function createPage(passedBrowser, linkUrl, customize) { const phone = allDevices[mockDevice || 'iPhone 6'] const pages = await passedBrowser.pages() const page = pages[0] // const page = await passedBrowser.newPage()await page.emulate(phone)if (customize && typeof customize === 'function') { await page.goto(linkUrl) await customize(page) } else { await page.goto(linkUrl, customize || pageConfig) }return page }async function closeBrowser() { if (browser) { await browser.close() browser = null } if (wsEnd) { wsEnd = null } }module.exports = { createBrowser, createPage, closeBrowser }

单元测试文件 test.js
const { createBrowser, createPage } = require('../scripts/puppeteer-opts') const { getStringFromTmpFile } = require('../scripts/tmp-file-opts') const waitTime = require('../scripts/util-wait-time') const { customInfo } = require('../config') const { waitSSOcall } = require('./test-aa-models')let linkUrl = null let browser = null let page = nullconst inputInfo = customInfojest.setTimeout(300 * 1000)describe('Puppeteer Sample', () => { beforeAll(async () => { const otaDeepLink = getStringFromTmpFile('otaDeepLink') linkUrl = otaDeepLinkconsole.log('**link from cache**', linkUrl)browser = await createBrowser() page = await createPage(browser, linkUrl, { waitUntil: 'domcontentloaded', timeout: 5 * 60 * 1000 })await waitSSOcall(page, '/um/v2/users/') })it('should open search result page success', async () => { const listNodes = await page.$$('a[class^=Button__ButtonContainer]') console.log('**search list leng**', listNodes.length) expect(listNodes.length).toBeGreaterThanOrEqual(1) })afterAll(async () => { await browser.close() page = null browser = null }) })

3. 结果呈现 excel的结果展示如下
微信小程序自动化测试的实践
文章图片

【微信小程序自动化测试的实践】html的结果展示如下
微信小程序自动化测试的实践
文章图片

4. 注意事项
  1. 所有的授权需要手动触发预先完成
  2. 元素selector只能支持元素跟样式,并且不能有多级
  3. 选择自定义组件下面的元素需要在样式前端加上组件名作为前缀
  4. 不能覆盖web-view
  5. 需要扫码登录后才能使用dev-tool作为自动化测试终端

    推荐阅读