詳解小程式自動化測試

coldplay.xixi
發布: 2020-08-21 17:11:47
轉載
3357 人瀏覽過

詳解小程式自動化測試

【相關學習推薦:微信小程式教學

背景

」近期團隊打算做一個小程式自動化測試的工具,期望能夠做到業務人員操作一遍小程式後,自動還原之前的操作路徑,並且捕獲操作過程中發生的異常,以此來判斷這次發布是否會影響小程式的基礎功能。

詳解小程式自動化測試

上述描述看似簡單,但是中間還是有些難點的,第一個難點就是如何在業務人員操作小程式的時候記錄操作路徑,第二個困難點就是如何將記錄的操作路徑進行還原。

自動化 SDK

如何將操作路徑還原這個問題,首選官方提供的 SDK: miniprogram-automator

小程式自動化 SDK 為開發者提供了一套透過外部腳本操控小程式的詳解小程式自動化測試,從而實現小程式自動化測試的目的。透過該SDK,你可以做到以下事情:

  • 控制小程式跳到指定頁面
  • #取得小程式頁面資料
  • 取得小程式頁面元素狀態
  • 觸發小程式元素綁定事件
  • 往AppService 注入程式碼片段
  • 呼叫wx 物件上任意介面
  • ...

上面的描述都來自官方文檔,建議閱讀後面內容之前可以先看看官方文檔,當然如果之前用過puppeteer ,也可以快速上手,api 基本一致。以下簡單介紹下 SDK 的使用方式。

// 引入sdkconst 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 中捕捉所有的事件,好在小程式所以的頁面和元件都必須透過PageComponent 方法來包裝,所以我們可以改寫這兩個方法,攔截傳入的方法,並判斷第一個參數是否為event 對象,以此來捕獲所有的事件。

// 暂存原生方法const originPage = Pageconst originComponent = Component// 改写 PagePage = (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)
}// 改写 ComponentComponent = (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></view><view></view><!-- 构建后 --><view></view><view></view>复制代码
登入後複製

但是取得到 class 之後,又會有另一個坑,小程式的自動化測試工具並不能直接取得頁面裡自訂元件中的元素,必須先取得自訂元件。

<!-- Page --><toast></toast><!-- Component --><view>
  <text>{{text}}</text>
  <view></view></view>复制代码
登入後複製
// 如果直接查找 .toast-close 会得到 nullconst element = await page.$('.toast-close')
element.tap() // Error!// 必须先通过自定义组件的 tagName 找到自定义组件// 再从自定义组件中通过 className 查找对应元素const element = await page.$('toast .toast-close')
element.tap()复制代码
登入後複製

所以我們在建置操作的時候,還需要為元素插入 tagName。

<!-- 构建前 --><view></view><toast></toast><!-- 构建后 --><view></view><toast></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 文本複製出來後,就可以透過自動化工具運行了。

// 引入sdkconst 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。

相關學習推薦:微信公眾號開發教學

#

以上是詳解小程式自動化測試的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:juejin.im
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
作者最新文章
最新問題
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板