> 技术文档 > 微信小程序自动化测试工具工具Minium全攻略(手把手教学)_微信小程序 minium

微信小程序自动化测试工具工具Minium全攻略(手把手教学)_微信小程序 minium


零 背景

在这个数字化时代,大大小小的企业都推出了自己的微信小程序,小程序的测试就成了逃不开的课题。最常用的小程序测试方法是用微信官方的工具,一般有Minium框架(python脚本)和miniprogram-automator方案(javascript脚本),这里具体讲解Minium框架方案。

Minium方案分为本地测试和云测,

1. 二者的测试共同点如下

1. 都需要安装微信开发者工具

2. 都需要登录开发者权限账号

3. 都需要python测试脚本

4. 都可以跨安卓、苹果、鸿蒙系统,mac也可以用,支持多种机型

2. 二者的差异如下

Minium云测与本地测试差异点比较 云测 本地测试 是否支持蓝牙测试 不支持连接到本地蓝牙设备,因为云测的真机在服务器上,而云测虚拟机没有蓝牙功能 真机测试支持蓝牙测试,虚拟账号测试不支持蓝牙测试 测试速度 快 普通界面调整比较快,但蓝牙相关的界面操作很慢,实测跳转界面一次需要半分钟 环境搭建 不需要搭建环境,直接测 需要搭建本地python项目环境 测试成本 每周有150分钟免费额度 全免费

网上有很多Minium云测的教程,这里不造旧轮子,给出官网链接:

微信开发文档/Api/小程序云测

主要讲下Minium本地测试

环境搭建的教程也很多,这里给出一个教程的链接

Minium本地测试环境搭建

一 项目结构

根目录下有:case文件夹、outputs文件夹、config.json、suite.json

(1)case文件夹用于保存测试用例

(2)outputs文件夹用于保存自动生成的测试报告

(3)suite.json与config.json保存配置信息

config.json

{ \"project_path\": \"AppPath\", \"dev_tool_path\": \"微信web开发者工具Path\\\\cli.bat\", \"debug_mode\": \"debug\", \"platform\": \"Android\", \"device_serial\": {}, \"remote_timeout\": 120, \"use_debug2\": true}

1. \"project_path\"请手动改为小程序源码的地址,也就是说本地必须有源代码

2.  \"dev_tool_path\"请手动改为微信开发者工具的地址

3. \"debug_mode\"不同,测试时输出的日志就不一样,debug比较常用,也可以用info

4. \"platform\"可以填Android\\iOS等

5. 本地测试一定要加\"device_serial\"属性,虚拟机测试不用加这个属性。本地只连接一台设备时,属性可以为空,本地有多台设备时,要配置serial属性

6. \"remote_timeout\"为超时时间,自行配置

7. \"use_debug2\": true表示使用真机测试2.0,不配置这个只能用真机测试1.0,具体用2.0还是1.0要根据小程序的微信公共库版本确定,现在一般的小程序都是用2.0的

详情请见官方文档Minium本地测试config.json配置详解

suite.json

{\"pkg_list\": [{ \"case_list\": [ \"test_*\" ], \"pkg\": \"case.*_test\" } ]}

这里用到了正则表达式,\"pkg\": \"case.*_test\"表示选择全部case文件夹内以“_test”结尾的python文件,\"case_list\": [\"test_*\"  ]表示选择这些python文件中以\"test_\"开头的方法

二 测试用例的常用功能

import minium,os,time,platformfrom minium import Callbackclass InitTest(minium.MiniTest): #测试所用手机需要先注册一个“博远之家”小程序的账号后,进入使用界面 def test_generator(self): #self.native.start_wechat() #self.mini.clear_auth() #清除用户授权 self.logger.info(\"开始测试\") self.native.allow_get_location() # 允许获取用户地理位置 self.app.wait_for_page(\"pgh\") self.ble_connection() #开启蓝牙 #下一步 if self.page.wait_for(\".guideNextBtn_StepOne\",max_timeout=2): next = self.page.get_element(\".guideNextBtn_StepOne\") next.click() for i in range(4): if self.page.wait_for(\".guideNextBtn_StepTwo\", max_timeout=2):  next = self.page.get_element(\".guideNextBtn_StepTwo\")  next.click() #点击添加设备 self.capture(\"菜单界面\") addDev = self.page.get_element(\".PlusSign\",max_timeout=10) addDev.click() #点击进入发电机 self.click_recent_device_by_text(self.page,\"发电机\") self.select_picker_item(\"BY&3404111111\") #设置参数 self.goSetting() self.set_slider_value(\"自启时间\",1,3) #校验参数 self.page.go_back() self.goParameter() self.check_li_right_text(\"自启时间\",\"5s\") self.page.go_back() # 设置参数 self.goSetting() self.set_slider_value(\"自启时间\", 0,3) # 校验参数 self.page.go_back() self.goParameter() self.check_li_right_text(\"自启时间\", \"3s\") self.page.go_back() def click_recent_device_by_text(self,page,text): \"\"\"通过按钮文本点击\"\"\" \"\"\"\"添加设备\"\"\" self.app.wait_for_page(\"pg0\") self.capture(\"选择设备页面截图\") page.wait_for(\'view.center_btn\', max_timeout=30)#等待界面加载 text_element = page.get_element(\'view.imgDesc\', inner_text=text) text_element.click() def select_picker_item(self,text): \"\"\"操作picker-view选择特定项并点击\"\"\" # 获取当前页面对象 self.app.wait_for_page(\'/pg1\') self.capture(\"连接蓝牙页面截图\") # 1. 定位picker-view组件 picker_element = self.page.get_element(\"picker-view\",max_timeout=30) self.capture(\"滑动页面截图\") self.page.wait_for(5) # 2. 获取数据源并查找目标索引 device_list = self.page.data.get(\"deviceNameList_show\", []) target_index = None for i, item in enumerate(device_list): if item.get(\"showName\") == text: # 替换为实际目标文本 target_index = i break # 未找到目标的处理 if target_index is None: self.logger.error(\"未找到目标选项\") return # 3. 触发picker-view滚动到目标位置 # value为数组格式,表示各列选择项的索引(从0开始) picker_element.trigger(\"change\", {\"value\": [target_index]}) # 4. 等待选择器滚动完成(根据实际情况调整等待时间) self.page.wait_for(1) # 等待1秒确保动画完成 # 5. 定位并点击目标选项 # 通过文本定位目标view(使用跨组件选择器确保准确) target_element = picker_element.get_element( \"view\", inner_text=text, # 替换为实际目标文本 max_timeout=20 ) target_element.click() # 触发bindtap事件 self.logger.info(f\"已选择第{target_index}项:{text}\") # 6. 验证选择结果(可选) # 示例:检查页面数据是否更新 # selected_value = page.data.get(\"selectedDevice\") # self.assertEqual(selected_value, \"XXXXXXXXXX\") def ble_connection(self): # 1. 初始化蓝牙 init_res = self.app.call_wx_method(\"openBluetoothAdapter\") if \"errCode\" in init_res.get(\"result\", {}): self._handle_bluetooth_failure(init_res) # 2. 启动设备发现 self.app.call_wx_method(\"startBluetoothDevicesDiscovery\", { \"services\": [], \"allowDuplicatesKey\": False }) def goSetting(self): self.app.wait_for_page(\"/packageUserPage/pages/control/control\") btn = self.page.get_element(\'.parameter\',max_timeout=20)#注意,设置参数确实是paramter按键,而参数页面是set btn.click() def goParameter(self): self.app.wait_for_page(\"/packageUserPage/pages/control/control\") btn = self.page.get_element(\'.set\', max_timeout=20) btn.click() def set_slider_value(self, box_top_text, target_index, cnt):#cnt为滑块上点的数量 \"\"\" 根据box_top的文本,定位对应的滑块控件,设置其为第target_index个值 :param box_top_text: box_top的文本(如“自启时间”) :param target_index: 目标滑块的索引(0、1、2等) \"\"\" # 步骤1:定位目标box_top元素(区分不同控件) self.app.wait_for_page(\"/p_old\") self.page.wait_for(2) #box_top = self.page.get_element(\".title\",inner_text=box_top_text, max_timeout=15)#box_top为BaseElement,没有parent属性 # 步骤2:找到该box_top所在的box容器(父级元素), self.capture(\"设置参数前\") # 步骤3:在box容器内定位目标sliding #slider = box.get_element(\'.sliding\',max_timeout=10) #element.get_element方法是否有效待验证 path = f\"\"\" //view[contains(concat(\' \', normalize-space(@class), \' \') and contains(concat(\' \', @class, \' \'), \' title \') and normalize-space(text())=\'{box_top_text}\'] /ancestor::view[position()=1] /view[contains(concat(\' \', @class, \' \'), \' level \')] /view[contains(concat(\' \', @class, \' \'), \' sliding \'] \"\"\" slider = self.page.get_element(path, max_timeout=20) # 步骤4:点击按钮触发切换 slider_rect = slider.rect tract_width = slider_rect.width target_x = slider_rect.x + (target_index * tract_width/(cnt-1)) target_y = slider_rect.y + slider_rect.height/2 #点击 slider.tap(x=target_x, y=target_y) self.page.wait_for(4) self.capture(f\"设置参数后{box_top_text},{target_index}\") def check_li_right_text(self, li_left_text, expected_li_right_text): \"\"\" 校验页面中指定 `li_left` 对应的 `li_right` 文本(忽略大小写) :param li_left_text: 要匹配的左侧标签文本(如 \"自启时间\" \"自启模式\") :param expected_li_right_text: 预期的右侧文本(如 \"5s\" \"自动\") \"\"\" # ================= 步骤1:定位 li_left 元素 ================= # 通过 CSS 选择器匹配文本为 li_left_text 的 .li_left 元素 self.app.wait_for_page(\"/packageUserPage/pages/parameter_old/parameter_old\") self.page.wait_for(2) li_left = self.page.get_element( \'.li_left\', inner_text = li_left_text, max_timeout=10, # 等待元素加载超时(秒) ) self.capture(f\"读取参数{li_left_text},{expected_li_right_text}\") if not li_left: raise ValueError(f\"未找到左侧标签为「{li_left_text}」的元素!\") # ================= 步骤2:找到父级  容器 ================= path = f\"\"\" //view[contains(concat(\' \', normalize-space(@class), \' \') and contains(concat(\' \', @class, \' \'), \' li \')  and .//view[contains(concat(\' \', @class, \' \'), \' li_left \') and normalize-space()=\'{li_left_text}\'] /view[contains(concat(\' \', @class, \' \'), \' li_right \')] \"\"\" li_right = self.page.get_element(xpath=path,max_timeout=20) # 向上查找最近的 class=\"li\" 的父元素 if not li_right: raise ValueError(f\"在列表项容器内未找到右侧文本元素{expected_li_right_text}!\") # 获取实际文本(去首尾空格,避免空白干扰) actual_text = li_right.get_attribute(\"text\").strip() actual_lower = actual_text.lower() # 转小写 expected_lower = expected_li_right_text.lower() # 预期文本转小写 # ================= 步骤4:校验文本(忽略大小写) ================= if actual_lower == expected_lower: print(f\"✅ 参数校验成功:「{li_left_text}」的右侧文本匹配(实际:{actual_text},预期:{expected_li_right_text})\") else: raise AssertionError( f\"❌ 参数校验失败:「{li_left_text}」的右侧文本不匹配!\\n\" f\"实际(小写):{actual_lower}\\n\" f\"预期(小写):{expected_lower}\" ) def tearDown(self): \"\"\"测试结束后自动执行的清理逻辑\"\"\" try: # 1. 关闭小程序 if hasattr(self, \"mini_program\"): self.mini_program.close() # 2. 关闭 Minium 驱动(强制释放端口) if hasattr(self, \"driver\"): self.driver.quit() system = platform.system() if system == \"Windows\": os.system(\"taskkill /F /IM wechatdevtools.exe\") # 强制杀工具进程 elif system == \"Darwin\": # macOS os.system(\"pkill -f WeChatWebDevTools\") except Exception as e: self.logger.warning(f\"资源释放失败:{e}\")