Home > Article > WeChat Applet > Detailed explanation of small program automated testing
[Related learning recommendations: WeChat Mini Program Tutorial]
The team plans to do a mini program automated test in the near future The tool is expected to enable business personnel to automatically restore the previous operation path after operating the mini program, and capture exceptions that occur during the operation to determine whether this release will affect the basic functions of the mini program.
The above description seems simple, but there are still some difficulties in the middle. The first difficulty is how to record the operations when business personnel operate the mini program. path, the second difficulty is how to restore the recorded operation path.
How to restore the operation path to this problem, the official SDK is preferred: miniprogram-automator
.
The Mini Program Automation SDK provides developers with a set of solutions to control mini programs through external scripts, thereby achieving the purpose of automated testing of mini programs. Through this SDK, you can do the following:
The above descriptions are all from official documents. It is recommended to read the official documents before reading the following content. Of course, if you have used puppeteer before, you can also get started quickly. The API is basically the same. The following is a brief introduction to how to use the 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() })复制代码
There is one thing that needs to be reminded: before using the SDK, you need to open the service port of the developer tools, otherwise the startup will fail.
With the method of restoring the operation path, the next step is to solve the problem of recording the operation path.
In the mini program, it is not possible to capture all events in the window through event bubbling like in the web. Fortunately, all pages and components of the mini program must pass Page
, Component
method to wrap, so we can rewrite these two methods, intercept the incoming method, and determine whether the first parameter is an event
object to capture all events .
// 暂存原生方法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) } }复制代码
The code here only proxies all event methods and cannot be used to restore the user's behavior. To restore the user's behavior, you must also know whether the event type is required, such as click, long press, and input.
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) } }复制代码
After determining the event type, you still need to clarify which element was clicked. However, the pitfall in the mini program is that there is no class name of the element in the target attribute of the event object, but the element can be obtained. dataset.
In order to obtain the elements accurately, we need to add a step in the construction, modify the wxml file, and change the class## of all elements # Copy the attribute to
data-className.
<!-- 构建前 --><view></view><view></view><!-- 构建后 --><view></view><view></view>复制代码But after obtaining the class, there will be another pitfall. The automated testing tool of the mini program cannot directly obtain the elements in the custom component in the page. The custom component must be obtained first.
<!-- 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()复制代码So when we build the operation, we also need to insert tagName for the element.
<!-- 构建前 --><view></view><toast></toast><!-- 构建后 --><view></view><toast></toast>复制代码Now we can continue happily recording user behavior.
// 记录用户行为的数组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) } }复制代码Up to this point, all the user’s clicks, inputs, and enter-related operations have been recorded. However, the scrolling screen operation is not recorded. We can directly proxy the
onPageScroll method of Page.
// 记录用户行为的数组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) }复制代码There is an optimization point here, that is, when the scroll operation is recorded, you can judge whether the last operation was also a scroll operation. If it is the same operation, you only need to modify the scroll distance, because the two The second scroll can be done in one step. In the same way, the same is true for input events, and the input value can also be reached in one step. Restore user behaviorAfter the user operation is completed, the json text of the user behavior can be output on the console. After copying the json text, it can be run through the automation tool.
// 引入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() })复制代码This is just a simple restoration of the user's operation behavior. During the actual operation, network requests and localstorage mocks will also be involved, which will not be described here. At the same time, we can also access the jest tool to make it easier to write use cases.
For seemingly difficult needs, as long as you explore them carefully, you can always find a corresponding solution. In addition, there are really many pitfalls in the automation tools of WeChat mini programs. If you encounter problems, you can first go to the mini program community to find them. Most of the pitfalls have been stepped on by predecessors. There are also some problems that cannot be solved at the moment and you can only find other ways to solve them. avoid. Finally, I wish the world is bug-free.
Related learning recommendations: WeChat public account development tutorial
The above is the detailed content of Detailed explanation of small program automated testing. For more information, please follow other related articles on the PHP Chinese website!