ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

微信小程序自动化测试实战:Appium+Python+Android真机

微信小程序自动化测试实战:Appium+Python+Android真机 说实话第一次接到微信小程序自动化测试需求时我第一反应是这不就是给微信里头的网页写自动化吗真正动手才发现小程序既不是普通 H5也不完全是原生 App“套壳”两个字根本没法概括它的复杂度。这篇文章我打算用 Android 真机 Appium Python 这套组合把微信小程序自动化从环境搭建、capabilities 配置、上下文切换到用例落地完整讲一遍也把我在实施过程中踩过的版本坑、定位坑和稳定性坑一并说出来。适合已经会用 Appium 跑过简单 App、想把手伸到小程序自动化这个方向的人看如果你刚接触 Appium也可以先照着搭建环境遇到问题时再回来看对应的排查思路。1. 小程序自动化为什么比普通 App 麻烦先搞清楚被测对象长什么样1.1 小程序的运行形态不是网页也不是原生 App很多人以为小程序就是“微信里的网页”但实际上它不是浏览器直接打开的 H5。小程序从微信入口进入之后渲染层跑在微信自己启动的一个 WebView 进程里Android 上对应的进程名经常是类似com.tencent.mm:appbrand0这样的东西。也就是说你通过 Appium 去连接微信Appium 眼里看到的还是一个叫微信的 App小程序内部页面并不是一个独立的被测应用。这个差异带来的直接结果是如果你只用appPackage和appActivity启动微信然后想用原来的原生控件定位方法去找小程序页面里的“确认订单”“立即支付”这类元素大概率什么都找不到。因为这些元素不在原生控件树里而是在 WebView 的 DOM 树里。想操作它得先做上下文切换。这一点是所有小程序自动化方案的认知基础不理解它后面会一直绕弯路。还有一个容易被忽略的点小程序的双线程模型让逻辑层和渲染层分离页面里大量使用view、text、button这类自定义组件而不是传统网页里的div、span。所以你在 WebView 上下文里看到的页面源码和你平时用 Selenium 抓普通网页的 HTML 结构不一样。定位策略可以沿用 XPath、CSS Selector 那套思路但元素名和属性要重新摸索不能照着普通网页的经验直接套。1.2 为什么选 Appium Python 而不是 Airtest / 其他方案我见过不少团队做小程序自动化时身边人会推荐 Airtest、AutoJS、甚至是截图脚本理由通常是“更快”。这些方案在小程序领域确实有人用但它们的定位和应用场景差别挺大方案元素识别方式适用场景主要短板Appium Python通过原生控件树或 WebView DOM 定位小程序功能回归、多步骤业务流程环境搭建复杂上下文切换需要理解Airtest图像识别为主游戏、UI 冒烟、跨端演示UI 样式一变脚本就挂断言能力弱AutoJSAndroid 无障碍服务安卓手机自动化脚本、快速打包操作做正式测试报告和持续集成比较费劲我最终选 Appium Python主要是因为它能同时兼顾原生层和 WebView 层而且 Python 生态里和 pytest、Jenkins、Allure 的整合都很成熟。团队如果有人写过 Selenium理解成本会很低。小程序页面里的按钮既然在 WebView 里那用 Selenium 那套元素等待和断言经验基本能平移过来。1.3 这套方案适合什么不适合什么适合做的场景很明确小程序核心流程回归、电商下单、表单填写、列表加载、页面跳转这类能靠元素判断业务结果的用例。脚本跑在真机上和用户实际使用环境高度一致。不适合做的场景也要心里有数小程序内部如果需要频繁做验证码、人脸识别、实名认证这类场景自动化成本极高不建议硬刚。还有一类是纯 Canvas 渲染的页面比如某些活动页、游戏模块元素树里可能只有一个 canvas 节点Appium 拿不到内部文字。遇到这种页面要么用截图 OCR 的方式辅助要么接受只能坐标点击的现实。另外不要拿生产环境的大号去跑自动化微信风控、支付风险提示、真实下单退款都会让你的脚本和账号都很痛苦。2. 环境搭建最容易翻车的不是 Appium 本身而是版本链2.1 一套按顺序装下来的清单安装 Appium 本身不复杂复杂的是所有配套组件能不能对上版本。我的建议是先把基础运行环境按顺序装齐再安装 Appium 相关的服务和 Python 库。需要准备的东西大致是这样的JDK 8 或更高版本很多 Android 工具链依赖它Android SDK至少包含platform-tools和build-toolsNode.js 14 以上Appium 2.x 是跑在 Node 上的Appium 服务端通过npm install -g appium安装Python 3.8 以上pip install Appium-Python-ClientAppium Inspector用来查看控件树和调试定位一台 Android 真机或模拟器装完之后先用命令行确认一下java -version adb --version node -v appium --versionAppium 2.x 和 1.x 有一个关键差异2.x 默认不再内置uiautomator2驱动需要手动安装appium driver install uiautomator2如果忘了这一步启动会话时会直接报错Could not find a driver for automationName: UiAutomator2。2.2 chromedriver 与 WebView 版本匹配半个环境报错都出在这里小程序页面是 WebViewAppium 在切换到 WebView 上下文时需要调起 chromedriver。而 chromedriver 和 WebView 内核版本必须匹配否则就会看到类似这样的报错unknown error: ChromeDriver only supports Chrome version xx很多新手在这里卡住以为是 Appium 没装好其实只是 WebView 内核和当前使用的 chromedriver 对不上。处理思路是这样先不要手动指定chromedriverExecutable让 Appium 自己选一个内置的 chromedriver 试试。如果报了版本不匹配再根据报错里的版本号去 chromedriver 下载列表里找对应版本。把下载好的 chromedriver 路径加进 capabilities 里的appium:chromedriverExecutable例如D:/chromedriver/chromedriver.exe。如果仍然报错用appium --verbose启动服务看它实际调用的 chrome driver 路径和启动参数。还有一个容易踩的坑是微信已经开始使用自研的渲染内核部分新版本微信的 WebView 调试行为会有变化导致 Appium 拿到上下文但页面源码空白。这时候先升级 Appium 和Appium-Python-Client到最新版本再排查 chromedriver 版本。版本组合这种事没有一劳永逸的答案关键是把报错日志看仔细它通常已经把答案写在里面了。2.3 真机连接与 adb 状态确认Android 真机连接这块最基础也是最容易忽略的adb devices如果看到unauthorized说明手机没有授权这台电脑。在手机上确认“允许 USB 调试”的弹窗必要时先撤销授权再重插。真机上建议把“开发者选项”里的“屏幕保持唤醒”打开并且关掉所有窗口动画也就是把“窗口动画缩放”“过渡动画缩放”“动画程序时长调整”都设为 0。动画看起来不重要但会影响元素是否可点击的判断也会让显式等待的节奏变乱。使用模拟器也可以但微信和模拟器之间偶尔会有兼容性问题。我自己的经验是优先用真机特别是需要测试微信支付、地理位置、摄像头这类系统能力时模拟器的行为不可靠。3. 配置 capabilities 并让 Appium 把微信打开看得见的坑与看不见的坑3.1 一份能跑的 Android 配置小程序自动化里capabilities 既有原生 App 的套路又有 WebView 场景的额外要求。下面这份配置在实际项目中是可用的注意里面的属性名带上了 Appium 2.x 的前缀from appium import webdriver desired_caps { platformName: Android, appium:platformVersion: 10, appium:deviceName: real_device, appium:appPackage: com.tencent.mm, appium:appActivity: .ui.LauncherUI, appium:noReset: True, appium:unicodeKeyboard: True, appium:resetKeyboard: True, appium:automationName: UiAutomator2, appium:showChromedriverLog: True } driver webdriver.Remote(http://127.0.0.1:4723/wd/hub, desired_caps)noReset: True很关键。它保证测试时不会去重置微信的本地数据微信登录状态能保留下来否则每次跑脚本都要重新扫码登录非常痛苦。但你也别因此用生产账号专门准备一个小号或者测试白名单账号最稳。unicodeKeyboard和resetKeyboard主要是为了解决中文输入问题。小程序里经常要输入手机号、姓名、地址这两个配置能避免 send_keys 输入中文时丢字或变成拼音的问题。3.2 从微信桌面进入小程序搜索、历史记录还是手动坐标真正考验脚本稳定性的往往不是进入小程序之后而是“进入小程序”这个动作本身。微信没有公开一个类似 deep link 的接口能直接从原生层唤起某个小程序页面所以常用的进入方式有三种方式一测试手机上手动把小程序打开过一次然后脚本用noReset: True继续跑小程序会出现在微信的最近使用列表里脚本通过固定坐标或列表项点击进入。方式二从微信首页顶部搜索框搜索小程序名称再点击搜索结果。方式三把小程序添加到“我的小程序”或桌面入口用坐标或元素点击。方式二最常见示例代码大概是import time driver.start_activity(com.tencent.mm, .ui.LauncherUI) time.sleep(4) # 顶部搜索框1080x2400 分辨率下大概在这个位置 driver.tap([(540, 190)]) time.sleep(2) search_input driver.find_element(xpath, //android.widget.EditText) search_input.send_keys(你的小程序名称) driver.press_keycode(66) time.sleep(5) # 点击搜索结果实际坐标要按本机分辨率调整 driver.tap([(540, 700)])这段代码里最脆弱的点是坐标。不同手机分辨率不同同一个手机不同微信版本首页布局也可能不同。我在项目里通常会把“进入小程序”单独抽成一个函数先在目标手机上用 Appium Inspector 抓一次真实控件节点能定位就优先用元素定位不到才用坐标。如果是开发阶段的体验版还要考虑测试环境的限制。体验版小程序不是每个人都能直接搜到需要开发在“微信开发者工具”里设置对应的测试成员或者通过预览二维码进入。脚本不适合处理每次都要扫码的场景更稳妥的做法是把被测对象固定为一个已经上线的测试小程序或者让开发提供一个稳定的体验版页面。3.3 为什么一直切不到 WEBVIEW微信调试开关第一排查顺序我遇到过不少同事跑脚本进入小程序后driver.contexts打印出来只有[NATIVE_APP]于是怀疑 Appium 装错。其实这时候最优先检查的是小程序到底有没有开启 WebView 调试。微信内部的小程序页面虽然跑在 WebView 里但默认对外不暴露调试端口。Appium 能拿到上下文前提是你先让这个 WebView 变成“可调试”的状态。常规做法是如果是自己开发的小程序在微信开发者工具里打开“详情 - 本地设置 - 开启调试”如果是测试别人开发的体验版需要开发在小程序后台或代码里做相应的调试配置。检查顺序建议是先在真机上手动打开小程序再用driver.contexts看结果。如果只有NATIVE_APP回去确认调试开关是否打开。确认后彻底杀掉微信进程再重进小程序注意微信对 WebView 进程会有回收和复用不杀进程直接看可能拿不到新上下文。还是不行打开showChromedriverLog看 Appium 启动 chromedriver 时有没有报权限、路径、版本类错误。有时候开发会反馈“我在开发者工具里明明打开了调试”但真机上还是拿不到原因是开发者工具的和真机的调试配置不是同一个开关。以真机实际表现为准不要只看开发者工具的界面状态。4. 上下文切换与元素定位小程序在上层其实还有一层 WebView4.1 切换上下文的完整代码进入小程序后第一步不是急着找元素而是确认上下文。完整的切换流程可以这样写from appium.webdriver.common.appiumby import AppiumBy from selenium.webdriver.support.ui import WebDriverWait # 等待出现 WEBVIEW 上下文 WebDriverWait(driver, 20).until(lambda d: len(d.contexts) 1) print(driver.contexts) for ctx in driver.contexts: if ctx.startswith(WEBVIEW): driver.switch_to.context(ctx) break time.sleep(2) print(driver.page_source[:3000])切换成功后小程序页面就不再是黑盒子。你可以用page_source把当前页面结构打印出来先看一遍再写定位。实际项目里看到的上下文名可能是WEBVIEW_com.tencent.mm:appbrand0之类的多个 WebView 同时存在时要注意别切错。还有个小坑如果在小程序里跳到微信公众号文章、外部 H5上下文可能会变成另一个 WebView 实例。此时如果脚本还用老的上下文操作元素会找不到。稳妥的做法是在每个核心步骤前写一个“确保处于目标上下文”的小函数而不是只在最开始切一次。4.2 元素定位text、view 和 class 的坑小程序在 WebView 里的 DOM 结构和普通 H5 不太一样写定位时不能想当然。我最常用的几种定位写法# 文本定位 driver.find_element(AppiumBy.XPATH, //text[contains(text,立即支付)]).click() # 按 class 和顺序定位 driver.find_element(AppiumBy.XPATH, //view[contains(class,goods-item)][1]).click() # 如果元素不在原生态视图里试试 css selector driver.find_element(AppiumBy.CSS_SELECTOR, .button-primary).click()第一个要注意的地方是小程序压缩编译后class 名很可能不是你写的那个名字wxss 里的类名会被处理成很短的随机字样。所以不要凭前端代码里的 class 去写定位一定要从page_source或者 Appium Inspector 的实际树里复制。第二个坑是文本定位偶尔会因为同一个文案出现多个节点而失败。比如“立即支付”这个文案可能同时出现在可视区和隐藏区域。这时候别硬写一个元素先看len(driver.find_elements(...))是几再决定用[0]还是[-1]。第三个坑是部分弹窗里的“同意”“允许”按钮可能是原生层弹窗也可能是 WebView 里的弹窗。原生层的元素要在NATIVE_APP上下文里找WebView 里的要在WEBVIEW上下文里找。两边的定位代码不通用遇到点击无效时先看上下文对不对。4.3 弹窗和授权自动化跑挂的重灾区小程序最烦人的是各种一次性弹窗用户协议、隐私政策、定位授权、手机号授权、订阅消息。这些弹窗第一次跑可能出现第二次因为状态保留就不出现。如果脚本写死每次都要点反而会点不到。我一般会写一个“存在则点击”的小工具from selenium.webdriver.support import expected_conditions as EC def click_if_present(locator, timeout3): try: el WebDriverWait(driver, timeout).until( EC.element_to_be_clickable(locator) ) el.click() return True except Exception: return False # 每次进入页面后先尝试关掉这些弹窗 click_if_present((AppiumBy.XPATH, //text[contains(text,同意)])) click_if_present((AppiumBy.XPATH, //text[contains(text,允许)]))这样的好处是脚本不会因为弹窗的不确定性而频繁挂掉。但要注意不能把所有按钮都无脑塞进“存在则点击”里否则脚本连“立即支付”都可能被悄悄点掉覆盖不到真实用户路径。5. 从“能点”到“能跑完”输入、滑动、坐标兜底与稳定性细节5.1 输入操作中文输入和清空是一个隐藏难点小程序里的输入操作最常见的就是表单填写。WebView 上下文里的输入框和普通 Selenium 输入框用法类似input_el driver.find_element( AppiumBy.XPATH, //input[placeholder请输入手机号] ) input_el.clear() input_el.send_keys(13800138000)但有几个地方和原生 App 不一样clear()在部分 WebView 输入框里不一定奏效可能清了之后光标还在或者旧文字没被真正删掉。稳妥做法是clear()之后加一个退格键操作或者连续send_keys空字符串。中文输入依赖unicodeKeyboard: True但如果手机本身装了输入法Appium 切换输入法时可能会弹出系统提示。这类问题在真机上尤其明显必要时可以用adb shell ime list -s来确认当前输入法。输入回车键不要用driver.press_keycode(66)硬写有的页面会把回车事件触发为“换行”而不是“搜索”。更可靠的方式是点击页面里的搜索按钮或者在输入后直接点击搜索图标。5.2 长列表、下拉刷新和“滑到某个元素出现”小程序里最常见的长列表操作就是首页推荐流、订单列表、商品列表。Appium 的swipe可以直接用但每次都写一坨坐标会很难看。我习惯封装一个按比例滑动的方法def swipe_up(driver, times1, start_ratio0.8, end_ratio0.2, duration500): size driver.get_window_size() w size[width] h size[height] start_y int(h * start_ratio) end_y int(h * end_ratio) for _ in range(times): driver.swipe(int(w / 2), start_y, int(w / 2), end_y, duration) time.sleep(1)下拉刷新则是反过来从屏幕上半部分向下滑driver.swipe(int(w / 2), int(h * 0.3), int(w / 2), int(h * 0.8), 300)如果列表很长最好写一个“滑到某个元素出现”的循环def swipe_until_text(driver, text, max_swipes5): for _ in range(max_swipes): try: return driver.find_element( AppiumBy.XPATH, f//*[text{text}] ) except Exception: swipe_up(driver) return None这里有个细节swipe 的速度和时间会直接影响结果。duration500表示 500ms 完成滑动属于比较慢的拖拽如果页面是惯性滚动你只滑一次它还会继续滚一会儿所以滑动后必须等 1 秒左右再找元素。太快太慢都会让“滑动加载”的触发方式不一样。5.3 坐标兜底与 Canvas 页面能不用就不用但必须会有些小程序页面会把关键内容画进 Canvas 里元素树里看不到可点击的文字。这时候除了截图 OCR最直接的手段就是坐标点击。坐标点击的代码很简单size driver.get_window_size() w size[width] h size[height] driver.tap([(int(w * 0.5), int(h * 0.8))])但坐标点击有很明显的缺点不同分辨率手机点位会变。所以我通常不是写死像素而是用屏幕宽高比例计算。这样至少能保证同一个设备或者分辨率相近的设备上稳定一些。还有一类按钮可能同时存在 DOM 节点和原生层手势Appium 点击报“可点击”但实际没有触发。这时候可以在driver.tap()前先driver.get_screenshot_as_file(before.png)点击后再截图对比看看页面是否真的发生了变化。做小程序自动化截图不是给领导看的而是给自己排查问题用的。6. 把脚本写成能长期交付的用例等待、断言、报告和重试6.1 不要用 sleep 堆用例显式等待是底线很多 Appium 新手写小程序自动化喜欢每一步都time.sleep(5)。这种写法不是不能用而是会让脚本变得很慢而且一旦页面加载超过 5 秒就直接挂掉。更好的方式是显式等待from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC def wait_clickable(locator, timeout15): return WebDriverWait(driver, timeout).until( EC.element_to_be_clickable(locator) ) wait_clickable( (AppiumBy.XPATH, //text[contains(text,提交订单)]) ).click()显式等待的核心价值是“元素可点才点击不可点就一直等”。这能大大减少因为网络慢、图片加载慢带来的不稳定。6.2 把脚本组织成 pytest 用例失败自动重跑脚本能跑还不够能交付才是重点。我通常用 pytest 组织用例加上pytest-rerunfailures做失败重跑import pytest from appium import webdriver pytest.fixture(scopemodule) def app_driver(): caps { platformName: Android, appium:deviceName: real_device, appium:appPackage: com.tencent.mm, appium:appActivity: .ui.LauncherUI, appium:noReset: True, appium:automationName: UiAutomator2, } driver webdriver.Remote(http://127.0.0.1:4723/wd/hub, caps) yield driver driver.quit() def test_submit_order(app_driver): loc (AppiumBy.XPATH, //text[contains(text,提交订单)]) wait_clickable(app_driver, loc).click() assert WebDriverWait(app_driver, 20).until( EC.presence_of_element_located( (AppiumBy.XPATH, //text[contains(text,支付成功)]) ) )跑的时候加上pytest --reruns 2 --reruns-delay 3重跑不是“治疗一切不稳定”的万能药但对于网络波动、弹窗延迟这类偶发问题很有效。如果某个用例重跑三次还是挂那基本就是真实 bug 或者定位方式写错了得回去修脚本。6.3 断言不要只判断“元素在不在”要判断“业务结果发生了”断言这块最常见的错误是只判断某个元素存在。比如点了“提交订单”然后就断言订单详情页出现。但“出现”也有可能是因为报错弹窗挡住了页面或者页面跳转失败后仍然有个残留节点。更贴合业务的做法是断言关键业务结果支付成功后是否出现“支付成功”或“订单已完成”列表滑动后某个商品名是否真的出现表单提交后是否出现“提交成功”的提示如果页面有 toast可以直接断言 toast 文案assert 提交成功 in driver.page_source这句话虽然简单但很多时候比判断某个元素可点击更有效。page_source是当前页面结构的完整快照只要文案出现就能确认业务流程走到了预期位置。6.4 截图、日志和测试数据隔离小程序自动化跑多了之后真正让人头疼的不是脚本写不出来而是脚本挂了以后不知道怎么排查。我的习惯是三件事固定下来用例失败时自动截图存到按日期命名的目录里。每个关键步骤后把当前上下文和page_source前 500 字打印到日志。测试数据必须是独立的不能和真实用户混在一起。截图可以在 pytest 的 hook 里做pytest.hookimpl(tryfirstTrue, hookwrapperTrue) def pytest_runtest_makereport(item, call): outcome yield report outcome.get_result() if report.failed or report.skipped: driver item.funcargs.get(app_driver) if driver: driver.get_screenshot_as_file( ffail_{item.name}_{int(time.time())}.png )测试数据隔离这点尤其重要。不要用真实手机号去接验证码不要用生产账号去下单付款。既保护账号安全也避免自动化脚本给线上业务制造脏数据。我在实际项目里还有一个个人习惯每次小程序发版后先跑一遍核心冒烟用例同时把页面源码保存一份和上一个版本对比一下元素树变化。这个动作看起来很笨但对维护自动化脚本帮助非常大。小程序前端改版频繁与其每次等脚本挂了再去修不如把“页面结构会变”当成预期中的事从第一天就做好定位和日志的模块化这样后面维护起来会轻松很多。
返回列表