小程序自动化测试

news2024/12/23 12:30:36

背景

近期团队打算做一个小程序自动化测试的工具,期望能够做到业务人员操作一遍小程序后,自动还原之前的操作路径,并且捕获操作过程中发生的异常,以此来判断这次发布是否会影响小程序的基础功能。

上述描述看似简单,但是中间还是有些难点的,第一个难点就是如何在业务人员操作小程序的时候记录操作路径,第二个难点就是如何将记录的操作路径进行还原。

自动化 SDK

如何将操作路径还原这个问题,首选官方提供的 SDK: miniprogram-automator

小程序自动化 SDK 为开发者提供了一套通过外部脚本操控小程序的方案,从而实现小程序自动化测试的目的。通过该 SDK,你可以做到以下事情:

  • 控制小程序跳转到指定页面
  • 获取小程序页面数据
  • 获取小程序页面元素状态
  • 触发小程序元素绑定事件
  • 往 AppService 注入代码片段
  • 调用 wx 对象上任意接口

上面的描述都来自官方文档,建议阅读后面内容之前可以先看看官方文档,当然如果之前用过 puppeteer ,也可以快速上手,api 基本一致。下面简单介绍下 SDK 的使用方式。

// 引入sdk
const automator = require('miniprogram-automator')

// 启动微信开发者工具
automator.launch({
  // 微信开发者工具安装路径下的 cli 工具
  // Windows下为安装路径下的 cli.bat
  // MacOS下为安装路径下的 cli
  cliPath: 'path/to/cli',
  // 项目地址,即要运行的小程序的路径
  projectPath: 'path/to/project',
}).then(async miniProgram => { // miniProgram 为 IDE 启动后的实例
	// 启动小程序里的 index 页面
  const page = await miniProgram.reLaunch('/page/index/index')
  // 等待 500 ms
  await page.waitFor(500)
  // 获取页面元素
  const element = await page.$('.main-btn')
  // 点击元素
  await element.tap()
	// 关闭 IDE
  await miniProgram.close()
})

有个地方需要提醒一下:使用 SDK 之前需要开启开发者工具的服务端口,要不然会启动失败。

捕获用户行为

有了还原操作路径的办法,接下来就要解决记录操作路径的难题了。

在小程序中,并不能像 web 中通过事件冒泡的方式在 window 中捕获所有的事件,好在小程序所以的页面和组件都必须通过 Page 、Component 方法来包装,所以我们可以改写这两个方法,拦截传入的方法,并判断第一个参数是否为 event 对象,以此来捕获所有的事件。

// 暂存原生方法
const originPage = Page
const originComponent = Component

// 改写 Page
Page = (params) => {
  const names = Object.keys(params)
  for (const name of names) {
    // 进行方法拦截
    if (typeof obj[name] === 'function') {
      params[name] = hookMethod(name, params[name], false)
    }
  }
  originPage(params)
}
// 改写 Component
Component = (params) => {
  if (params.methods) {
      const { methods } = params
      const names = Object.keys(methods)
      for (const name of names) {
        // 进行方法拦截
        if (typeof methods[name] === 'function') {
          methods[name] = hookMethod(name, methods[name], true)
        }
      }
  }
  originComponent(params)
}

const hookMethod = (name, method, isComponent) => {
  return function(...args) {
    const [evt] = args // 取出第一个参数
    // 判断是否为 event 对象
    if (evt && evt.target && evt.type) {
      // 记录用户行为
    }
    return method.apply(this, args)
  }
}

这里的代码只是代理了所有的事件方法,并不能用来还原用户的行为,要还原用户行为还必须知道该事件类型是否是需要的,比如点击、长按、输入。

const evtTypes = [
    'tap', // 点击
    'input', // 输入
    'confirm', // 回车
    'longpress' // 长按
]
const hookMethod = (name, method) => {
  return function(...args) {
    const [evt] = args // 取出第一个参数
    // 判断是否为 event 对象
    if (
      evt && evt.target && evt.type &&
      evtTypes.includes(evt.type) // 判断事件类型
    ) {
      // 记录用户行为
    }
    return method.apply(this, args)
  }
}

确定事件类型之后,还需要明确点击的元素到底是哪个,但是小程序里面比较坑的地方就是,event 对象的 target 属性中,并没有元素的类名,但是可以获取元素的 dataset。

为了准确的获取元素,我们需要在构建中增加一个步骤,修改 wxml 文件,将所有元素的 class 属性复制一份到 data-className 中。

<!-- 构建前 -->
<view class="close-btn"></view>
<view class="{{mainClassName}}"></view>
<!-- 构建后 -->
<view class="close-btn" data-className="close-btn"></view>
<view class="{{mainClassName}}" data-className="{{mainClassName}}"></view>

但是获取到 class 之后,又会有另一个坑,小程序的自动化测试工具并不能直接获取页面里自定义组件中的元素,必须先获取自定义组件。

<!-- Page -->
<toast text="loading" show="{{showToast}}" />
<!-- Component -->
<view class="toast" wx:if="{{show}}">
  <text class="toast-text">{{text}}</text>
  <view class="toast-close" />
</view>

// 如果直接查找 .toast-close 会得到 null
const element = await page.$('.toast-close')
element.tap() // Error!

// 必须先通过自定义组件的 tagName 找到自定义组件
// 再从自定义组件中通过 className 查找对应元素
const element = await page.$('toast .toast-close')
element.tap()

所以我们在构建操作的时候,还需要为元素插入 tagName。

<!-- 构建前 -->
<view class="close-btn" />
<toast text="loading" show="{{showToast}}" />
<!-- 构建后 -->
<view class="close-btn" data-className="close-btn" data-tagName="view" />
<toast text="loading" show="{{showToast}}" data-tagName="toast" />

现在我们可以继续愉快的记录用户行为了。

// 记录用户行为的数组
const actions = [];
// 添加用户行为
const addAction = (type, query, value = '') => {
  actions.push({
    time: Date.now(),
    type,
    query,
    value
  })
}

// 代理事件方法
const hookMethod = (name, method, isComponent) => {
  return function(...args) {
    const [evt] = args // 取出第一个参数
    // 判断是否为 event 对象
    if (
      evt && evt.target && evt.type &&
      evtTypes.includes(evt.type) // 判断事件类型
    ) {
      const { type, target, detail } = evt
      const { id, dataset = {} } = target
    	const { className = '' } = dataset
    	const { value = '' } = detail // input事件触发时,输入框的值
      // 记录用户行为
      let query = ''
      if (isComponent) {
        // 如果是组件内的方法,需要获取当前组件的 tagName
        query = `${this.dataset.tagName} `
      }
      if (id) {
        // id 存在,则直接通过 id 查找元素
        query += id
      } else {
        // id 不存在,才通过 className 查找元素
        query += className
      }
      addAction(type, query, value)
    }
    return method.apply(this, args)
  }
}

到这里已经记录了用户所有的点击、输入、回车相关的操作。但是还有滚动屏幕的操作没有记录,我们可以直接代理 Page 的 onPageScroll 方法。

// 记录用户行为的数组
const actions = [];
// 添加用户行为
const addAction = (type, query, value = '') => {
  if (type === 'scroll' || type === 'input') {
    // 如果上一次行为也是滚动或输入,则重置 value 即可
    const last = this.actions[this.actions.length - 1]
    if (last && last.type === type) {
      last.value = value
      last.time = Date.now()
      return
    }
  }
  actions.push({
    time: Date.now(),
    type,
    query,
    value
  })
}

Page = (params) => {
  const names = Object.keys(params)
  for (const name of names) {
    // 进行方法拦截
    if (typeof obj[name] === 'function') {
      params[name] = hookMethod(name, params[name], false)
    }
  }
  const { onPageScroll } = params
  // 拦截滚动事件
  params.onPageScroll = function (...args) {
    const [evt] = args
    const { scrollTop } = evt
    addAction('scroll', '', scrollTop)
    onPageScroll.apply(this, args)
  }
  originPage(params)
}

这里有个优化点,就是滚动操作记录的时候,可以判断一下上次操作是否也为滚动操作,如果是同一个操作,则只需要修改一下滚动距离即可,因为两次滚动可以一步到位。同理,输入事件也是,输入的值也可以一步到位。

还原用户行为

用户操作完毕后,可以在控制台输出用户行为的 json 文本,把 json 文本复制出来后,就可以通过自动化工具运行了。

// 引入sdk
const automator = require('miniprogram-automator')

// 用户操作行为
const actions = [
  { type: 'tap', query: 'goods .title', value: '', time: 1596965650000 },
  { type: 'scroll', query: '', value: 560, time: 1596965710680 },
  { type: 'tap', query: 'gotoTop', value: '', time: 1596965770000 }
]

// 启动微信开发者工具
automator.launch({
  projectPath: 'path/to/project',
}).then(async miniProgram => {
  let page = await miniProgram.reLaunch('/page/index/index')
  
  let prevTime
  for (const action of actions) {
    const { type, query, value, time } = action
    if (prevTime) {
      // 计算两次操作之间的等待时间
  		await page.waitFor(time - prevTime)
    }
    // 重置上次操作时间
    prevTime = time
    
    // 获取当前页面实例
    page = await miniProgram.currentPage()
    switch (type) {
      case 'tap':
  			const element = await page.$(query)
        await element.tap()
        break;
      case 'input':
  			const element = await page.$(query)
        await element.input(value)
        break;
      case 'confirm':
  			const element = await page.$(query)
 				await element.trigger('confirm', { value });
        break;
      case 'scroll':
        await miniProgram.pageScrollTo(value)
        break;
    }
    // 每次操作结束后,等待 5s,防止页面跳转过程中,后面的操作找不到页面
    await page.waitFor(5000)
  }

	// 关闭 IDE
  await miniProgram.close()
})

这里只是简单的还原了用户的操作行为,实际运行过程中,还会涉及到网络请求和 localstorage 的 mock,这里不再展开讲述。同时,我们还可以接入 jest 工具,更加方便用例的编写。

总结

看似很难的需求,只要用心去发掘,总能找到对应的解决办法。另外微信小程序的自动化工具真的有很多坑,遇到问题可以先到小程序社区去找找,大部分坑都有前人踩过,还有一些一时无法解决的问题只能想其他办法来规避。最后祝愿天下无 bug。


公众号粉丝福利

  • 软件测试全套资源免费领取

  • 软件测试面试刷题小程序免费使用

  • 专属于测试人的GPT免费使用

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/615322.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

为什么黑客要攻击你的网站?如何才能保护网站不被攻击?

根据2023年一季度应用程序安全状况报告所披露的报告&#xff0c;今年来全球已经累计有超过1400多万个网站遭受了超过10亿次网络攻击&#xff0c;网络的安全风险依然在逐年不断提升。 几乎每个网站都面临风险&#xff0c;无论是简单的博客论坛、投资平台、小型的独立电商网站还是…

无需服务器,5分钟在公众号中接入ChatGPT

前言 在原先使用openAI的接口分别实现过微信聊天&#xff0c;语音对话等功能的基础上&#xff0c;我又将矛头指向了公众号&#xff0c;最近在github中找到了一个挺好玩的案例&#xff1a;公众号机器人&#xff0c;于是打算分享一下整个搭建过程 准备工作 微信公众号AirCode账…

三、基尔霍夫定理

目录 基本概念 基尔霍夫电流定理&#xff08;KCL&#xff09; 基尔霍夫电压定理&#xff08;KVL&#xff09; 总结 基本概念 1.支路 定义1&#xff1a;电路中每一个两端元件就称为一条支路 定义2&#xff1a;电路中通过同一电流的分支 2.结点 定义1&#xff1a;元件的连接…

spring实例化bean之循环依赖

serviceA里注入了serviceB&#xff0c;serviceB里又注入了serviceA&#xff0c;这样在实例化serviceA时&#xff0c;在doCreateBean时的populateBean时会先实例化serviceB&#xff0c;然后实例化serviceB&#xff0c;在serviceB的doCreateBean方法的populateBean又会去找servci…

什么是Linux shell—一个简单的案例

一句话概括&#xff1a;简单来说脚本就是将需要执行的命令保存到文本中&#xff0c;按照顺序执行&#xff08;由上往下执行&#xff09;&#xff0c;shell脚本:shell脚本就是一些命令的集合。 一、创建第一个Shell脚本&#xff1a;输出helloworld 1&#xff0e;脚本格式 脚本…

(转载)基于遗传模拟退火的聚类算法(matlab实现)

1 理论基础 1.1 模糊聚类分析 模糊聚类是目前知识发现以及模式识别等诸多领域中的重要研究分支之一。随着研究范围的拓展&#xff0c;不管是科学研究还是实际应用&#xff0c;都对聚类的结果从多方面提出了更高的要求。模糊C-均值聚类(FCM)是目前比较流行的一种聚类方法。该…

【不单调的代码】还在嫌弃Ubuntu终端?快来试试做些Ubuntu终端的花式玩法。

&#x1f38a;专栏【​​​​​​​不单调的代码】 &#x1f354;喜欢的诗句&#xff1a;更喜岷山千里雪 三军过后尽开颜。 &#x1f386;音乐分享【Love Story】 &#x1f970;大一同学小吉&#xff0c;欢迎并且感谢大家指出我的问题&#x1f970; 注意&#xff1a; 本文是在…

【Protobuf速成指南】Protobuf快速上手

文章目录 1.0版本一、编写.proto文件1.文件规范&#xff1a;2.注释方式&#xff1a;3.指定proto3语法&#xff1a;4.package申明符5.定义message6.编写消息字段①类型对照表②唯一编号 二、编译.proto文件1. 编译指令2.源码分析 三、序列化和反序列化的使用四、小结 1.0版本 本…

TCP连接管理与UDP协议

一、TCP的连接管理 1.TCP包头 2.连接的建立——“三次握手” TCP 建立连接的过程叫做握手。 采用三报文握手&#xff1a;在客户和服务器之间交换三个 TCP 报文段&#xff0c;以防止已失效的连接请求报文段突然又传送到了&#xff0c;因而产生 TCP 连接建立错误。 3.连接的释放…

【智能座舱】— 看上海车展,高端品牌变局,时代变天早开始,40项智能化创新技术解密~

大家好,欢迎阅读本期文章,我们将带您解读一份极具实用价值的汽车研究报告。本期将聚焦于2023年上海车展,解密未来座舱发展技术脉络 在本期文章中,我们将深度探讨这些前沿技术的应用,为您呈现未来汽车的全景图。我们相信,这将有助于您更加准确地选择适合自己的新能源汽车…

Kafka测试实战:从基础入门到高阶技巧(建议收藏)

Kafka是一种高吞吐量的分布式发布-订阅消息系统&#xff0c;它可以处理所有活动流数据。在进行Kafka的测试时&#xff0c;我们需要验证生产者能否成功发送消息&#xff0c;消费者能否成功消费消息。在本文中&#xff0c;我们将使用Python来进行Kafka的测试&#xff0c;并提供从…

pytorch实战 -- 数据加载和处理

Pytorch提供了许多工具来简化和希望数据加载&#xff0c;使代码更具可读性。这里将专门讲述transforms数据预处理方法&#xff0c;即数据增强。 数据增强又称为数据增广、数据扩增&#xff0c;它是对训练集进行变换&#xff0c;使训练集更丰富&#xff0c;从而让模型更具泛化能…

POWERBUILDER中高级学习提纲

Chengg0769 2012年 版权来自于&#xff1a; www.mis2erp.com http://blog.csdn.net/chengg0769 http://www.haojiaocheng.cc 转载请保留以上信息 这个提纲的来由&#xff1a; 当时&#xff0c;有个朋友说因伤疗养&#xff0c;想从过去做维护变为做开发&#xff0c;想学习…

从裸机启动开始运行一个C++程序(四)

先序文章请看 从裸机启动开始运行一个C程序&#xff08;三&#xff09; 从裸机启动开始运行一个C程序&#xff08;二&#xff09; 从裸机启动开始运行一个C程序&#xff08;一&#xff09; 跳转 前面我们介绍过&#xff0c;8086CPU总是在执行CS:IP所对应的内存位置的指令&…

签章那些事 -- 让你全面了解签章的流程

前言 随着通信、互联网技术的发展&#xff0c;人们接触到的信息纷繁复杂&#xff0c;信息的真真假假让人难以辨认。在严肃性场合&#xff0c;比如电子合同、电子证照等&#xff0c;必须有一种手段确保信息的完整性和真实性&#xff0c;这时签章就派上了用场。 签章的技术原理并…

高频面试八股文用法篇(四) 乐观锁和悲观锁的例子

目录 什么是乐观锁和悲观锁 乐观锁的实现方式主要有两种&#xff1a;CAS机制和版本号机制 1&#xff09;CAS&#xff08;Compare And Swap&#xff09; (2)版本号 乐观锁适用场景 乐观锁和悲观锁优缺点 功能限制 竞争激烈程度 什么是乐观锁和悲观锁 乐观锁&#xff1…

录音软件哪个好用?录音软件免费下载安装

案例&#xff1a;有没有好用的录音软件推荐&#xff1f; 【我想录制电脑上的音乐和音频会议&#xff0c;也想用电脑录制自己的歌声&#xff0c;有没有好用的电脑录音软件推荐&#xff1f;】 在日常生活和工作中&#xff0c;我们经常会遇到需要录音的场景&#xff0c;比如会议…

几行代码,轻松教你用Java 将 Word 文档转换为 HTML

Aspose.Words 是一种高级Word文档处理API&#xff0c;用于执行各种文档管理和操作任务。API支持生成&#xff0c;修改&#xff0c;转换&#xff0c;呈现和打印文档&#xff0c;而无需在跨平台应用程序中直接使用Microsoft Word。此外&#xff0c; Aspose API支持流行文件格式处…

基于windows环境利用VS下通过Linux环境下服务器进行UDP通信交流

目录 前言 Linux udpServer.cc udpServer.hpp makefile windows 细节1 -- 头文件引入 细节2 -- 固定写法 细节3 -- 结束后清理 细节4 -- socket返回值接受 细节5 -- 套接字创建(一样的写法) 细节6 -- 填写sockaddr_in结构体 细节7 -- 接发收数据 细节8 -- 报错信…