小程序UI自动化测试实践:Minium+PageObject
前阵子接手一个小程序自动化测试项目,踩了一路的坑,总算把Minium+PageObject这套方案稳定跑起来了。小程序UI自动化,做过的人都知道,跟Web自动化完全是两码事——没有DOM、没有WebDriver协议、开发者工具还频繁改版,用Selenium那套思路硬套基本是自讨苦吃。腾讯官方出的Minium算是目前最靠谱的方案之一,配合PageObject做页面对象建模,测试用例不再是密密麻麻的选择器堆叠,而是变得像业务操作说明书一样清晰。
这套方案能解决的核心问题有三个:一是小程序页面元素难以通过传统自动化方式定位,二是测试用例跟页面结构强耦合导致一改页面就炸一片用例,三是小程序本身多环境的差异化(安卓/iOS、真机/开发者工具)让自动化维护成本居高不下。如果你正在做小程序质量保障工作,或者公司的小程序项目开始要求自动化覆盖率指标,这篇内容可以帮你少走很多弯路。
1. 整体设计与方案选型思路
1.1 为什么是Minium而不是Appium或Airtest
先说结论:如果你测试的对象是微信原生小程序(不是uniapp打包的H5套壳),Minium是优先级最高的选择,没有之一。
我之前在另一个项目里用过Appium测小程序,那个体验是真的痛苦。小程序运行在微信这个宿主App里,Appium只能看到微信的页面层级,小程序内部是基于自研的渲染引擎,元素树天然不完整。你定位一个按钮,要么靠坐标硬点,要么用image对比,用例稳定性全看运气。Airtest虽然图像识别能力很强,但本质是"看像素",窗口大小一变、字体大小一调,整套用例就废了。
Minium不一样,它是微信小程序官方推出的自动化测试框架,直接基于小程序底层能力提供了一套类似Selenium的API。元素定位支持text、class、id、属性选择器,还支持直接调用小程序内部的方法和获取运行时数据,这在其他方案里是不可能做到的。另外它还内置了har抓包、Mock、全局data上报功能,对做数据驱动的测试场景非常友好。
1.2 PageObject模式在小程序环境下的适配
PageObject的核心思想用一个词概括就是"封装"。把每个页面当成一个独立的类,页面上所有的元素定位表达式和操作行为都封装在类内部,测试用例只跟页面类打交道,不直接接触选择器。这样做的好处是当页面结构调整时,只需要修改对应Page类,测试用例代码一行不用动。
但小程序环境有个特殊的地方:小程序页面栈和Web页面完全不同,一个页面可以由多个自定义组件组成,而且页面之间的跳转是依托小程序的navigation机制。所以做PageObject设计的时候,我采用了两层结构:顶层是Page类,对应小程序的一个原生页面;底层是Component类,对应页面内的自定义组件。比如一个商城主页有搜索框、商品列表、底部Tab栏三个自定义组件,我就拆分出SearchComponent、ProductListComponent、TabBarComponent三个类,主页Page类持有这三个组件的实例并组合对外暴露操作方法。这样拆分下来,用例写起来非常清爽。
1.3 这套组合的优势和边界
Minium+PageObject的组合,最明显的收益是用例的可读性和可维护性双双提升。我接手这个项目之前,老团队用裸Minium写用例,一个用例文件三百多行,全是定位表达式和点击操作逻辑,新人看半天也不知道这个用例到底在验证什么业务。重构之后,用例的平均长度压缩到原来的一半以下,而且基本是业务语言,比如login_page.input_username("test_user")、home_page.click_first_product()这种,即使不懂代码的产品和测试看到也能猜个大概。
当然,这套方案也不是银弹。Minium目前仅支持微信小程序,抖音小程序、支付宝小程序都覆盖不了。如果你的项目是跨端小程序(同一套代码编译到多个平台),那还需要对比下各框架对自家端的支持程度,或者用uni-app官方配套的方案。另外,Minium真机调试的功能相对有限,大部分场景还是在开发者工具里执行,这决定了它更适合做核心流程的回归验证,不太适合做真机性能类、网络弱网类的专项测试。
2. 环境搭建与核心配置
2.1 Minium的安装和依赖
Minium的环境搭建比想象中要稍微繁琐一点,主要因为它依赖微信开发者工具提供的自动化接口。下面这些都是我实际操作中一步步验证过的:
- 操作系统:Windows 10/11 或 macOS(Linux我没试过,官方文档没明确支持)
- Python版本:3.7 到 3.10 都可以,推荐用3.9,新版本3.11以上部分依赖不支持
- 微信开发者工具:必须安装,且版本最好选稳定版,RC版有时接口会变动
- 微信开发者工具账号:需要能登录,且必须开启服务端口
先装minium这个Python库,直接pip安装就行:
pip install minium这里有个小坑,minium有时候会带上一堆依赖,如果公司内网环境无法直接访问PyPI,你需要在配好的机器上把whl包都导出,再在内网环境装。建议用pip download -r requirements.txt -d ./packages这种离线下载方式准备。
安装完之后,核心是配置开发者工具。打开微信开发者工具的设置面板,找到“安全设置”,打开“服务端口”开关。这个端口就是自动化脚本和开发者工具通信的通道,不开启的话,minium无论如何都连不上。
2.2 配置init文件
minium运行时需要一个配置文件,官方标准做法是写一个minium.json放在项目根目录:
{ "dev_tool_path": "C:/Program Files (x86)/Tencent/微信web开发者工具/cli.bat", "project_path": "D:/work/your_miniprogram", "platform": "ide", "debug": false, "auto_launch": true, "enable_reuse": true }dev_tool_path是开发者工具CLI程序的路径,Windows环境一般是cli.bat,macOS是/Applications/wechatwebdevtools.app/Contents/MacOS/cliproject_path指向小程序项目目录platform用ide就行,真机调试需要改成ios或android,但需要额外配置enable_reuse是个好功能,能复用上一次启动的开发者工具实例,大幅提升用例执行速度
2.3 小程序的沙箱机制
小程序自动化有个天然的坑,就是微信登录态、缓存这些数据在开发者工具里是共享的。也就是说,如果上一个用例登录了账号A,下一个用例的session里还残留着账号A的信息。Minium官方提供了沙箱机制来解决这个问题,核心是Sandbox配置:
"options": { "sandbox": { "enable": true, "storage": "isolated", "cache": "isolated" } }开启了隔离之后,每个测试用例运行的storage和cache都是独立的,用例之间的数据干扰基本可以忽略不计。实测下来,这个配置对用例稳定性的提升非常明显。不过启用隔离之后,个别依赖本地存储做状态恢复的业务流程(比如扫码进入小程序后自动登录的场景),需要重新设计测试数据准备方式。
我个人的经验是:如果项目组刚接Minium,先把这个配置开好再写用例,否则后面用例多了再去排查数据污染问题,你会怀疑人生的。
3. PageObject模式设计与元素定位细节
3.1 基础Page类封装
我封装了一个基础的BasePage类,所有具体页面的Page类都继承它。这个类做的事情不多,但都是高频操,值得好好设计:
import minium class BasePage: def __init__(self, mini: minium.Mini, page_path: str): self.mini = mini self.page_path = page_path def get_page(self): """获取当前页面元素实例""" return self.mini.get_current_page() def click_element(self, selector: str, index: int = 0): """统一元素点击入口""" self.mini.wait_for(selector, timeout=5) self.get_page().get_element(selector, index).click() def input_text(self, selector: str, text: str): """统一输入操作""" element = self.get_page().get_element(selector) element.input(text) def assert_element_text(self, selector: str, expected_text: str): """断言元素文本""" actual = self.get_page().get_element(selector).text assert actual == expected_text, f"文本断言失败: 期望 {expected_text}, 实际 {actual}"里面有几个细节值得说一下:
wait_for是Minium里我最常用的一个方法,它会阻塞直到元素出现,超时才抛异常。所有点击操作之前我都加了这层等待,比硬编码sleep要稳得多。页面上元素还没渲染完就点击,在小程序里太常见了,尤其是网络差的时候。
get_element和get_elements的区别也要搞清楚。前者只取第一个匹配的元素,后者返回列表,可以用下标取指定位置的元素,比如商品列表里取第3个商品。写Page类的时候我会根据业务语义选择用哪种方式。
3.2 元素定位的几种方式
小程序页面的元素定位比Web要复杂一些,核心原因是小程序的自定义组件会改变节点的层级结构。我实际使用下来,常用的定位方式有这几种:
- 按text文本定位:
text属性,适合按钮、标题这类带文字的节点,比如text="立即支付" - 按class定位:
class属性,适合同一组件下不同状态的节点,比如class="active"表示高亮状态 - *按data-属性定位:小程序里大量使用自定义数据属性做事件绑定,比如
># 按text定位 self.mini.get_current_page().get_element("text=立即支付").click() # 按class定位 product = self.page.get_element(".product-card") product.get_element(".product-price").text # 按data属性定位 self.page.get_element('[data-code="1001"]').click()定位表达式里最实用的组合是
componentText和attribute组合。Minium文档里有一句话值得反复看:能不用坐标就不用坐标,能不用xpath就不用xpath。坐标在屏幕尺寸不一的手机上就是灾难,xpath在小程序这种组件化的结构下,可读性差且极易受结构变动影响。3.3 自定义组件的处理
微信小程序的页面是由页面级的wxml和一系列自定义组件组合而成的。Minium对自定义组件的支持是通过
get_element返回的元素链条一层层往下获取。我举个例子,一个典型的商品列表:
<product-list> <product-card class="card" wx:for="{{products}}" wx:key="code"> <view class="product-name">{{item.name}}</view> <view class="product-price">{{item.price}}</view> <button>class ProductListComponent: def __init__(self, page_element): self.element = page_element def get_product_by_code(self, code: str): return self.element.get_element(f'[data-code="{code}"]') def click_buy_by_code(self, code: str): self.get_product_by_code(code).get_element("button").click()这样一个商品列表的交互逻辑就全部封装在Component里了,用例代码只需要关心"我要买哪个商品",不需要知道商品在页面上怎么定位、按钮叫什么名字。
3.4 页面跳转和等待
小程序自动化翻页有个要注意的地方:跳转之后必须等待页面完全加载完成,否则后续操作会不稳定。Minium提供了
page_ready方法或者你可以手动等待特定元素出现:self.mini.redirect_to("/pages/payment/payment") self.mini.wait_for('text="确认支付"', timeout=5)这里有个小技巧:如果页面里没有合适的固定元素用来等待,可以在Page类里加一个隐藏的标记节点,比如在页面底部放一个
<view id="page-ready"></view>,然后等待这个节点出现。这个方法听着笨,但实测下来比time.sleep(2)这种写法稳定太多了。4. 实操流程:从用例编写到报告生成
4.1 测试用例的项目结构
我通常这样组织项目目录:
miniprogram_auto/ ├── minium.json # Minium 配置 ├── cases/ # 测试用例目录 │ ├── __init__.py │ ├── test_login.py # 登录流程用例 │ ├── test_home.py # 首页流程用例 │ └── test_payment.py # 支付流程用例 ├── pages/ # PageObject页面类 │ ├── __init__.py │ ├── base_page.py │ ├── home_page.py │ ├── login_page.py │ └── payment_page.py ├── components/ # 自定义组件封装 │ ├── product_list.py │ └── tab_bar.py └── reports/ # 测试报告输出目录这个结构和Web自动化项目很相似,团队成员能很快上手。cases目录放用例,pages放页面类,components放组件类,互不干扰。
4.2 一个完整的用例实例
以登录流程为例,我写一个典型的用例:
import minium from pages.login_page import LoginPage from pages.home_page import HomePage class TestLogin(minium.MiniTest): def test_login_success(self): """验证正确账号密码可以登录成功""" login_page = LoginPage(self) login_page.enter_username("18699999999") login_page.enter_password("password123") login_page.click_login_button() home_page = HomePage(self) home_page.wait_for_login_success() assert home_page.is_logged_in() is True def test_login_wrong_password(self): """验证错误密码提示正确""" login_page = LoginPage(self) login_page.enter_username("18699999999") login_page.enter_password("wrong_password") login_page.click_login_button() assert login_page.get_error_message() == "密码错误,请重新输入"看起来非常直观对吧?每个用例就是一个业务场景的操作步骤加断言。执行的时候,Minium会启动开发者工具,打开小程序项目,逐步执行这些操作。
4.3 数据准备与断言设计
小程序自动化测试里,数据准备是很有讲究的。我的经验是优先用小程序后端的Mock能力或者直接调用接口造数据,而不是通过UI去一个一个填。
Minium支持自定义
Mock配置,可以在请求层面拦截和返回假数据:"mocks": [ { "url": "**/api/login", "method": "POST", "response": { "code": 0, "data": { "token": "fake_token_123" } } } ]这个配置写在minium.json里,框架会在请求发出时自动匹配规则并返回配置的response,测试环境不稳定或者后端没有部署好的时候非常有用。我一般建议网络层全部Mock,UI层只验证交互和展示逻辑。
断言这一块,除了元素的文本断言外,我还经常用Minium的
get_data接口直接读取页面实例的data数据:page_data = self.mini.get_current_page().get_data() assert page_data["productList"][0]["name"] == "测试商品A"这种做法比定位UI元素再断言要快得多,而且不依赖页面的视觉渲染,对纯逻辑校验的场景非常高效。我在实际项目中是两种方式混合使用的:UI渲染问题用元素断言,数据逻辑问题用get_data断言。
4.4 报告与CI集成
Minium自带基于unittest的测试执行框架,用例跑完会生成一个HTML报告。在每个用例里加上
docstring描述,报告里会显示这些描述,方便做测试结果的链路追踪。在CI里集成的时候,我一般配置一个shell脚本,编译小程序和运行测试一条命令搞定:
#!/bin/bash cd /path/to/miniprogram npm install npm run build cd /path/to/auto_test minium run --report report.html执行完通过
echo $?判断进程退出码,非零则CI失败,这样测试失败能自动阻断发布流程。配合企业微信机器人或者飞书机器人做通知,效果更好。在实际落地时,我建议把自动化用例的执行频率控制在每分钟几次、核心用例每次发布前跑一遍,覆盖度先聚焦在冒烟和核心链路。不要一上来就想把所有的功能全部自动化,那不现实的,先把最核心的几条用户路径(登录、加购、下单、支付、退款)跑稳定,再逐步扩展。
5. 常见问题与排查技巧实录
5.1 连接失败:端口无法正常通信
现象:运行Minium时报错,提示
Fail to connect to the developer tools或者连接成功后立即断开。排查步骤:
- 检查开发者工具的服务端口是否已经打开(设置—安全设置—服务端口)
- 检查开发者工具账号是否已登录,未登录状态下自动化接口是不工作的
- 检查开发者工具版本和Minium版本兼容性,官网有版本对应表,务必对照确认
- 用命令行手动执行一下
cli.bat --auto-port看有没有报错信息
有一次我排查了整整半天,最后发现是开发者工具在升级后默认把服务端口关闭了,重新手动打开后一切正常。这个开关的位置藏得比较深,很多人找不到。
5.2 元素定位不到,但页面上明明能看到
现象:代码里定位某个按钮,报了找不到元素的错误,但手动打开页面,这个按钮看着就在那里。
原因:小程序页面里存在大量自定义组件,组件的内部阴影树(Shadow DOM)与页面级别的简化树(A11y树)并不一样。Minium默认获取的是a11y树,如果你定位的节点在一个没有正确暴露属性的组件内部,就很难被识别到。
解决思路:
- 尽量给组件节点加上
id、aria-label或># 打印当前页面的元素树,调试定位问题神器 elements = self.mini.get_current_page().get_elements("*") for elem in elements[:50]: print(elem)这个调试方法我在日常开发中太常用了。
5.3 用例串扰和缓存问题
现象:单独跑一个用例通过,把一串用例一起跑,前面的用例失败了,或者后面的用例登录状态变成了前面用例的用户。
原因:开发者工具复用时,小程序进程没有完全重置,全局变量、本地存储、登录态互相污染。
解决思路:
- 开启
enable_reuse时,务必同时开启沙箱隔离(storage/cache分离开) - 每个用例的开始和结束尽量做好前置清理工作和后置恢复工作
- 在
setUp里执行一次小程序的清理操作,比如重新启动或者清空storage
def setUp(self): super().setUp() self.mini.clear_storage() self.mini.restart()强制重启会拉长执行时间,但换来的稳定性提升值得。如果是少量用例,建议关掉
enable_reuse,每次独立跑,稳定优先。5.4 动态数据的固定化处理
现象:用例跑了几次之后开始随机失败,或者换了一批测试数据后失败。
原因:测试数据是实时生成的,比如下单后订单号是随机的,或页面上展示的时间是当前时间,断言写死了固定值。
解决思路:
- 测试数据尽量用Mock接口造稳定的数据
- 断言避免写死具体数值,改为断言数据结构或状态变化,比如"订单状态变为已支付"
- 如果必须要用到动态生成的ID,从页面data中读取,而不是写死
5.5 异常定位的最后一个大招:har抓包
Minium内置了har抓包能力,可以在用例执行时自动导出每个步骤的请求记录。当遇到"页面表现正常但断言失败"这种比较诡异的场景时,我会看一眼har文件,十次里有八次能定位到问题——要么是接口返回了预期的报错码,要么是某个请求超时被限流了。
开启方法很简单,minium.json里加:
"enable_har": truehar文件默认会输出到报告目录,用Chrome开发者工具的Network面板导入就能看。这对排查前后端联调类问题太有用了,你不需要在Web端或者手机上去抓包,用例执行的过程本身就是一次完整的操作链路。
5.6 常见问题速查表
问题现象 可能原因 解决建议 端口连接失败 服务端口未开启 / 版本不兼容 重新打开服务端口,检查版本对应表 元素定位超时 页面未加载或节点未渲染 加大wait_for超时时间,或等特定元素出现 定位到错误节点 class重复或选择器不精确 改用id或data属性,打印元素树确认 用例间数据干扰 storage/cache未隔离 开启sandbox隔离配置,清理storage 断言随机失败 数据动态变化 Mock数据或断言状态变化而非具体值 开发者工具进程残留 CLI启动失败或端口占用 杀掉微信开发者工具进程后重试 写在最后
这套Minium+PageObject的方案运行到现在,最大的感受就是:自动化测试的瓶颈从来不是工具,而是设计。工具再强,用例结构一团糟,维护成本自然会吃掉所有的收益。PageObject的意义不在于代码写得多花哨,而在于让测试用例回归"描述业务"的本职。
我踩过的另一个很深的坑是:一开始总想着把所有的用例都塞进自动化里跑,后来发现在小程序这种快速迭代的项目里,UI变化太频繁了,一些非核心页面用例过两个版本就要修,性价比极低。建议把自动化覆盖范围收敛到核心用户体验路径、高发缺陷模块和跨版本稳定模块这三类,把自动化的价值用在刀刃上。
如果看完这篇,你想上手试一下,建议周末花半天时间搭个Demo项目先跑通一个最最简单的用例——登录或者首页加载都行。跑通了再谈复杂场景,一点都不晚。
- 开启