微信小程序自动化测试的实践
团队开发小程序已经有一段时间了,随着开发的功能越来越多,我们的测试同学回归的任务也越发的重,所以我们决定用自动化测试来减轻一些回归测试的压力,同时也可以用来作为我们应用日常可访问性检查的一个工具,话不多说我们马上进入正题。
1. 方案确定
方案主要是围绕以下我们的几个需求
- 覆盖小程序
- 覆盖web-view
- 测试框架
- 原生页面与web-view页面的通讯
- 输出测试结果(excel, 使用html在线展示)
在查看了官方的使用文档后,发觉这个
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自定义reporter1.5 原生与web-view之间的通讯 鉴于我们的web-view的访问地址是需要原生的页面产生的,所以为了能够保证web-view可以直接使用这些地址,我们使用了一个临时的文本文件来存储这些地址信息,在puppeteer在加载这些页面的时候,我们会在临时的文本文件里边查找。
1.6 方案总结 经过上述的多个步骤,最终我们的自动化测试项目架构如下:
文章图片
2. 方案实施 2.1 配置小程序开发工具 在使用小程序官方提供的
miniprogram-automator
进行自动化测试,我们需要完成以下几个配置- 配置好小程序开发工具的cli目录,此目录一般是在小程开发工具的安装目录下,并且路径地址分格符需要使用
‘/’
无论window还是mac,如‘path/to/cli’ - 配置好小程序原码的地址
{
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"
}
- 打开小程序服务端口
文章图片
打开端口配置
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-scoke
t或者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-core
及miniprogram-automator
如何实现自动化代码的编写,相信大家就会明白了。- 使用
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)
})
})
- 使用
puppeteer-core
的参考实例
{
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. 注意事项
- 所有的授权需要手动触发预先完成
- 元素selector只能支持元素跟样式,并且不能有多级
- 选择自定义组件下面的元素需要在样式前端加上组件名作为前缀
- 不能覆盖web-view
- 需要扫码登录后才能使用dev-tool作为自动化测试终端
推荐阅读
- 小公司的前端建设的一些思考
- c/c++技术|小白 C++ 入门到大神发疯学习路线
- 程序员|阿里P7大牛手把手教你!mysql数据库应用案例教程
- 程序员|Java的Io模型你了解多少(dockerbuild指定版本)
- C++|C++应用程序列表(来自Bjarne Stroustrup)
- 教你用认知和人性来做最棒的程序员
- 上海航芯 | 从STM32F103到ACM32F403的U盘程序移植工程
- 培训出来的程序员,要怎么熬过进去公司的第一个月()
- 程序人生|十年AI学者影响力盘点(何恺明排名第一,成为全球最具影响力学者)
- Java应用结构规范