Тест автоматизации небольшой программы

Апплет WeChat тестовое задание

задний план

В ближайшее время команда планирует создать инструмент для автоматизированного тестирования небольших программ Ожидается, что после того, как бизнес-персонал один раз запустит апплет, будет автоматически восстановлен предыдущий путь работы, а исключения, возникающие во время работы, будут быть захвачены, чтобы судить, повлияет ли этот выпуск на работу.Основные функции апплета.

方案

Приведенное выше описание кажется простым, но посередине все еще есть некоторые трудности.Первая трудность заключается в том, как записать рабочий путь, когда бизнес-персонал работает с апплетом, а вторая трудность заключается в том, как восстановить записанный рабочий путь.

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 нужно открыть сервисный порт инструментов разработчика, иначе запуск не удастся.

开启服务端口

зафиксировать поведение пользователя

При методе восстановления рабочего пути следующим шагом является решение проблемы записи рабочего пути.

В апплете невозможно зафиксировать все события в окне через всплывающее окно событий, как в сети, к счастью, все страницы и компоненты апплета должны пройти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对象

Для того, чтобы получить элементы точно, нам нужно добавить шаг в построение, изменить файл 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>

Однако после получения класса будет другая яма.Инструмент автоматизированного тестирования апплета не может напрямую получить элементы в пользовательском компоненте на странице и должен сначала получить пользовательский компонент.

<!-- 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)
  }
}

До этого момента записывались все операции, связанные с кликами, вводом и возвратом каретки пользователя. Но до сих пор нет записи прокрутки экрана, мы можем напрямую проксировать Пейдж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()
})

Это всего лишь простое восстановление поведения пользователя при работе, в реальном запущенном процессе также задействованы сетевые запросы и mocks localstorage, которые здесь описываться не будут. В то же время мы также можем получить доступ к инструменту jest, который более удобен для написания вариантов использования.

Суммировать

Казалось бы, сложные требования, если исследовать их сердцем, всегда можно найти соответствующие решения. Кроме того, в средствах автоматизации WeChat Mini Programs действительно много ям. Если вы столкнулись с проблемами, вы можете обратиться в сообщество Mini Program, чтобы найти их. Большинство ям наступило на предшественников, и есть некоторые проблемы. это не может быть решено временно избегать. Наконец, я желаю миру никаких ошибок.