1. 程式人生 > 程式設計 >小程式自動化測試的示例程式碼

小程式自動化測試的示例程式碼

背景

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

小程式自動化測試的示例程式碼

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

自動化 SDK

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

小程式自動化 SDK 為開發者提供了一套通過外部指令碼操控小程式的方案,從而實現小程式自動化測試的目的。通過該 SDK,你可以做到以下事情:

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

上面的描述都來自官方文件,建議閱讀後面內容之前可以先看看官方文件 ,當然如果之前用過 puppeteer ,基本是無縫銜接。下面簡單介紹下 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 中捕獲所有的事件,好在小程式所以的頁面和元件都必須通過 PageComponent 方法來包裝,所以我們可以改寫這兩個方法,攔截傳入的方法,並判斷第一個引數是否為 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,value
 })
}

// 代理事件方法
const hookMethod = (name,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,value)
 }
 return method.apply(this,args)
 }
}

到這裡已經記錄了使用者所有的點選、輸入、回車相關的操作,但是還有一個滾動螢幕的操作還沒記錄。這裡可以直接監聽 Page 的 onPageScroll。

// 記錄使用者行為的陣列
const actions = [];
// 新增使用者行為
const addAction = (type,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(),value
 })
}

Page = (params) => {
 const names = Object.keys(params)
 for (const name of names) {
 // 進行方法攔截
 if (typeof obj[name] === 'function') {
  params[name] = hookMethod(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',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,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。

到此這篇關於小程式自動化測試的示例程式碼的文章就介紹到這了,更多相關小程式自動化測試內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!