1. 为什么移动应用自动化测试绕不开 Appium
前后端分离、快速迭代、多端适配,这些词在移动互联网项目里几乎天天能听到。作为测试工程师,如果还停留在手工点屏幕、反复回归主流程,项目节奏稍微一起来,很快就成了瓶颈。我自己这几年在移动端自动化上踩了不少坑,最后一直留着 Appium 这个工具没放,原因很简单:它能同时把 Android 和 iOS 捏到同一套技术栈里,而且社区的沉淀足够厚,遇到问题基本都能搜到解法。这篇文章就用一个真实项目从零到一的完成过程,把 Appium 这套东西给你讲透,包括环境搭建、元素定位、代码封装、调度执行,以及我在实际工作中踩过的各种奇奇怪怪的坑。
Appium 能做什么?简单说,它是一个基于 HTTP 协议的服务端程序,把移动端的原生控件操作转换成一套统一的标准接口,你用 Python、Java、JavaScript 这类熟悉的语言写测试脚本,脚本通过 Appium Server 转成对 Android 或 iOS 设备的真实操作指令,最后再由脚本去断言界面和业务逻辑是否符合预期。比如自动打开一个 App、点击登录按钮、输入账号密码、滑动列表、截屏比对,这些手工重复度极高的动作,都可以通过 Appium 脚本稳定地跑起来,还能接着接入 CI(持续集成)平台,在每次代码提交通道里自动完成回归验证。
什么人必须认真学这个?做了两三年移动端测试、想往高级测试开发或者自动化测试架构方向走的人,尤其适合。我见过不少同学,手工测试功底很强,业务也很熟,但一到自动化就觉得“不知道怎么下手”,其实就是缺一套完整的最小可运行骨架。另外,移动应用开发者也适合了解一下这套玩法,至少你在自测功能的时候,可以用脚本把一些主要链路跑一遍,不用每次都手动点来点去。Appium 的学习曲线不算陡,但坑是真的多,就好比做菜,食谱翻了几遍觉得都懂,真上灶才发现火候、锅具、油温全是变量。这篇文章我尽量把变量都提前摆出来。文章里的代码和配置没有特别指定某台设备,只基于常见实践,大家照着自己的设备环境做对应调整就能跑起来。
2. 移动端自动化测试的整体设计与技术选型
2.1 Appium 的核心架构与工作原理
在真正动手装环境之前,必须先搞明白 Appium 到底是怎么工作的。它的整体架构很像一层“中转站”:测试脚本运行在你的电脑上,使用 Appium 提供的 Client 库发起 HTTP 请求;Appium Server 收到请求后,根据不同移动端平台调用对应的驱动组件,最终把指令给到设备上的自动化引擎。Android 端走的是 UIAutomator2 或 Espresso 驱动,iOS 端走 XCUITest 驱动,而驱动在底层是通过系统对外开放的测试接口来操作控件树的。
这个设计带来的直接好处是跨平台统一。你写脚本的时候,不用关心设备内部到底用的什么技术去驱动,只需要面对 Appium 抽象出来的 API,比如 click、send_keys、 find_element、swipe 这些方法。从这个角度看,Appium 其实就把“操控移动设备”这件事变成了一套标准接口,跟 Web 自动化里的 Selenium 干的事非常像,只不过 API 更偏向移动端的触摸、滑动、多手势这些场景。
再说说 Appium 的两个重要版本,一个是老牌的 Appium 1.x,一个是 Appium 2.x。Appium 2.x 从设计上做了大调整,驱动本身拆掉了内置的移动端组件,改成按需安装的插件机制,比如需要跑 Android 就安装 uiautomator2 驱动,需要跑 iOS 就安装 xcuitest 驱动。刚开始上手的话,我建议直接用 Appium 2.x,因为你装一套环境只管自己需要的平台,比 1.x 省心不少,也顺带避开了一些旧版本的兼容性问题。官方还在持续更新 2.x,社区里新出的问题和修复也是优先落在 2.x 上。
2.2 为什么选择 Appium 而不是其他自动化方案
移动端自动化测试可选方案并不少。常见的有三大类,第一类是 Appium 这种跨平台黑盒方案,脚本与 App 源码无关,对着安装包和真实的界面做操作;第二类是 Android 平台独有的 UiAutomator 原生测试框架,还有 iOS 平台自带的 XCTest,它们通常需要和源码工程集成,在开发自测阶段用得多;第三类是一些商业工具或者云测平台,但往往需要付费,脚本语言和自由度也有限。
对比下来,Appium 的优势在于三点。一是技术栈自由,你可以用已经熟悉的 Python、Java、JavaScript 写测试脚本,不需要为了测试专门去学一套陌生的语言;二是 Appium Inspector 这类辅助工具成熟,可以非常直观地查看界面元素树、属性、坐标,对新手尤其友好;三是社区生态庞大,几乎所有网上能搜到的移动端自动化问题,都有人用 Appium 踩过并给出答案。当然,Appium 也不是没有代价,它走的是中间层转发,执行速度会比原生测试框架慢一些,如果你要跑上千条用例,对设备和执行机的性能要求会更高。这在绝大多数业务回归场景里是可以接受的,但如果你的项目对执行速度有极致要求,那就需要评估一下是否值得为了性能去绑到特定平台上。
在团队落地的时候,我见过的比较稳的组合是:Appium 负责端到端主干流程验证,加 pytest 管理用例和断言,再套上 Allure 做测试报告,最后用 Jenkins 定时拉起来跑回归。如果你的项目是 Java 体系,套 TestNG 或其他测试框架也一样,思路是通用的。这就引出了下一个问题,如何搭建一个既能跑通又能维护的自动化测试基础工程。
2.3 一个可落地的自动化测试工程应该长什么样
很多新手学 Appium 卡在“只会在单个脚本里点击”的状态,就是因为他没有建立“工程化”的概念。自动化测试不是把操作步骤写进一个 .py 文件然后 run,而是要把设备连接、App 启动、元素定位、业务操作、数据断言、报告输出、异常恢复这几块逻辑拆开。我习惯把工程分四层:第一层是公共配置层,专门放设备参数、App 路径、测试环境地址等全局信息;第二层是基础封装层,把启动 App、等待元素、点击、输入、滑动这些动作封成可复用的方法;第三层是页面对象层,按“一页面对应一个类”的方式,把某个界面的元素和交互方法集中起来;第四层是测试用例层,只写业务步骤和期望结果。
页面对象模式(也就是常说的 Page Object)我强烈建议早点养成习惯,别觉得“我这个项目就几个用例,直接写在脚本里省事”。真实项目里界面改动的频率比你预想的高,如果元素定位散落在各个用例里,一处数据属性变了,你得把所有脚本都翻出来改,而如果每个页面有专门的类,只需要改一个地方。这跟代码里把重复逻辑抽成公共函数是一个道理,前者是给测试代码做“结构化复用”。
好,架构层说透了,我们进入实战环节。接下来要装的东西比较多,但每一件都有明确用处,我会告诉你为什么一定要装,怎么验证装成功了,而不是只甩给你一串安装命令就完事。
3. 从零搭建 Appium 环境:工具链逐项拆解
3.1 准备 JDK 与 Android SDK
Appium 要操作 Android 设备,底层是通过 adb(Android Debug Bridge)来通信的,而 adb 又是 Android SDK 里的一个组件。所以 Android SDK 是必须的。同时 Appium 本身是 Java 写的,电脑上也要有 JDK。很多同学在这里第一步就被卡住,大多数原因是没有搞清楚 JDK 版本和 Android SDK 版本之间的配合关系。
我自己在 Windows 上测过一条比较省心的路径:先去官网装 JDK 17,然后用 IDE 或命令行跑一下java -version确认安装成功。Android SDK 的安装,我推荐用 Android Studio 来装,因为 Android Studio 的 SDK Manager 图形界面很直观,勾选几个必要组件就能完成,省去手动配环境变量的麻烦。装 Android Studio 倒不是为了写 App,纯粹是为了拿它自带的 SDK。SDK 装好后记住安装路径,后面配置环境变量的时候要用。
接下来配置 ANDROID_HOME 环境变量,这是 Appium 查找 Android 相关组件的关键变量。然后在 PATH 里加上platform-tools目录,这样命令行里就能直接用 adb 命令了。配置完成后,打开一个新的命令行窗口,分别执行adb version和appium-doctor(如果没有 appium-doctor 就先装一个,下面会提到)做一次体检。如果 adb 版本号正常打印出来,说明环境变量生效了。
这里有个小建议,尽量别用 Android Studio 自带的那个 jre,单独安装 JDK 在路径上更可控,后面排查问题的时候也更省事。命令行窗口在修改环境变量之后一定要重新开一个,因为新开的窗口才会加载最新的环境变量设置。
3.2 安装 Appium Server 与 Appium Inspector
Appium 的最新版本通常通过 Node.js 的 npm 包管理器来安装。如果你电脑上有 Node.js 环境,直接执行npm install -g appium就能把 Appium Server 装好。安装完成后,命令行执行appium --version确认版本。接着还需要为 Android 平台安装驱动,Appium 2.x 的最新驱动安装命令是appium driver install uiautomator2,如果你要测 iOS,就安装xcuitest驱动。这些驱动各自管理各自平台的协议转换,按需安装的好处是减少环境体积,也避免版本冲突。
有了 Server 还需要一个能和设备“对话”的可视化工具,就是 Appium Inspector。它主要干两件事:第一,连接设备后展示屏幕截图和界面控件树,帮你查看每个元素的 id、xpath、class、accessibility id 等定位信息;第二,可以在工具里直接试验点击、查找、滑动操作,快速验证你得出的定位表达式到底能不能定位到元素。我强烈建议新手把元素定位的调试都放在 Inspector 上完成,不要在脚本里一次一次试错,那样效率太低。
Appium Inspector 的获取方式有两种,一种是通过 GitHub 官方 Release 下载桌面版,另一种是直接在命令行里通过 npm 安装后再启动。下载桌面版的好处是可以独立运行,不占命令行资源。有一点容易踩坑:Inspector 连接设备时同样需要配置 Desired Capabilities(设备描述参数),而且它默认连接的端口是 4723,所以启动 Appium Server 时最好就用默认端口,避免不必要的烦恼。
3.3 配置 pytest 与 Python 开发环境
测试脚本这一侧,我选 Python + pytest,原因前面已经提过,上手快、写法简单、断言体系成熟。需要安装的第三方库主要有三个:Appium-Python-Client提供 Appium 的 Python 客户端方法;pytest负责用例收集、执行与断言;pytest-html或allure-pytest用来生成可视化的测试报告。
安装命令都很直接,用pip install Appium-Python-Client pytest pytest-html就能搞定。如果你想用 Allure 报告,还需要额外下载 Allure 命令行工具,并且配置到 PATH 里。这里有个小提醒:Appium-Python-Client的版本最好和 Appium Server 的大版本匹配,装完可以通过pip show Appium-Python-Client查看版本号,如果 Server 是 2.x,Client 也尽量用 2.x 以上,因为旧版 Client 很多接口签名在老版本上会变化,照着网上的教程写会报奇奇怪怪的错误。
还需要在电脑上准备一个真正的 Android 设备或者在 Android Studio 里启动一个模拟器。模拟器启动之后,通过adb devices检查设备状态,看到设备编号加上device状态,说明这个设备已经被识别,可以成为自动化操作的对象了。永远不要忽略 adb devices 这一步,后面大量问题的排查起点都在这里。
3.4 用 appium-doctor 检查环境完整性
在我教过的很多新人那里,环境没装干净是出现最多的问题。Appium 官方很贴心,提供了一个环境自检工具叫appium-doctor,安装方式就是npm install -g appium-doctor。装完以后,在命令行执行appium-doctor --android,它会检查 JDK、Android SDK、ANDROID_HOME、adb 等多个依赖项,每一项后面会有绿色打勾或者红色打叉的标记,打叉项如果显示了缺什么,照着补即可。
有的同学装完了发现 appium-doctor 提示找不到 Java,但是自己明明装了 JDK。这种情况绝大多数是因为安装 JDK 后没有配置JAVA_HOME环境变量。appium-doctor不是智能到去扫描你电脑上所有 Java 安装位置的,它只认环境变量指向的路径。所以建议配置完所有环境变量之后,再回头跑一遍 appium-doctor,确认每一项都有效,再继续往下走。排查环境问题的时候,别靠直觉,先跑这个工具,它能筛掉一大半低级的配置错误。
4. 第一次运行 Appium 自动化:设备连接与核心配置
4.1 理解 Desired Capabilities 的作用
Desired Capabilities 是 Appium 会话开始时的一组 JSON 键值对,用来告诉 Appium Server:要测什么平台、平台的版本是多少、要启动哪个 App、用哪种驱动等等。这组参数很像包裹的“寄件单”,Server 根据它找到合适的设备、合适的驱动、合适的启动方式。我第一次学 Appium 时忽略了这个概念,以为 setup 只是走个过场,结果后面所有“会话无法建立”的问题都跟它脱不开干系。
以 Android 为例,有几个必填项,platformName必须写成Android,appium:automationName指定为UiAutomator2,platformVersion是手机系统的 Android 版本号,deviceName可以是设备型号或 adb devices 里显示的标识符,app指向待测 App 的安装包路径。如果你要测的是已经安装在设备上的 App,就不传app,而是传appPackage和appActivity,说明你要启动哪个应用入口。传 apk 和传已装 App 的包名是两种不同模式,前者适合首次安装后测试,后者适合持续回归时省去重装 App 的时间。
另外还有几个可选但很实用的参数,appium:noReset设置为 true 表示不重置 App 的本地数据,这样登录状态和缓存可以保留;appium:unicodeKeyboard和appium:resetKeyboard通常配合使用,解决中文输入框无法键入文字的问题。真实项目里,这些参数会和项目环境、测试策略强相关,没有一套绝对固定的模板,你要理解每个参数干了什么,再按需调整。
4.2 从 adb 连接检查到启动 Appium 会话
开始跑脚本之前,设备侧要先通过 adb 打通连接。如果是真机,用数据线连上电脑后,在手机里打开开发者选项,开启 USB 调试;部分手机还需要在弹出授权框时点允许。连接后在命令行执行adb devices,你应该看到类似emulator-5554 device或一个真实的设备序列号。如果状态是unauthorized,通常是因为手机上没允许调试授权;如果状态是offline,多半是数据线不支持数据传输或者驱动有问题,换一根线或插拔一次试试。
接着启动 Appium Server。在命令行直接输入appium,它会默认监听4723端口。终端里出现 “Appium REST http interface listener started on 0.0.0.0:4723” 这种日志,就代表 Server 已经就绪。保持这个窗口开着,另开一个新窗口去运行你的 Python 脚本。
下面我用一段最简代码演示一次性创建会话、执行一个点击、再关闭会话的全过程。注意,这里的启动参数我只写了最常见的必填项,实际操作时你要按自己的设备和 App 情况补充:
from appium import webdriver caps = { "platformName": "Android", "appium:automationName": "UiAutomator2", "appium:deviceName": "emulator-5554", "appium:platformVersion": "12", "appium:appPackage": "com.example.app", "appium:appActivity": ".MainActivity", "appium:noReset": True, } driver = webdriver.Remote("http://127.0.0.1:4723", caps) # 找到页面上的登录按钮并点击 login_button = driver.find_element( by=AppiumBy.ID, value="com.example.app:id/btn_login" ) login_button.click() driver.quit()这段代码跑通,你的第一个 Appium 会话就算成功了。这里有几个容易犯错的地方,platformVersion填错会导致会话失败,虽然它看起来就是个版本号;appPackage和appActivity必须和实际安装包一致,你可以用adb shell dumpsys window | grep mCurrentFocus之类的命令查当前前台 App 的包名和界面名。如果第一次启动 App 的时候设置了取消动画、更改权限这些操作,画面加载会有延迟,代码里最好加上等待逻辑,这一点后面细讲。
4.3 借助 Appium Inspector 完成元素定位调试
在自动化测试里,元素定位是日常维护中最耗时的一环。Appium Inspector 就是帮你把界面上所有控件的属性摊开来,你就能找到可靠稳定的定位依据。打开 Inspector,填好 Appium Server 地址 4723,再填上和脚本里一致的 Capabilities,点启动会话,工具会连接设备并自动截取当前屏幕。左侧显示屏幕截图,右侧显示控件树,你点击左侧某个控件,右侧会给出它的详细属性,包括 text、resource-id、class、content-desc、bounds 这些。
定位一个元素,从稳定的属性入手,优先级通常是这样:content-desc(对应 accessibility id)最高,因为它通常是产品为无障碍功能设计的,比较固定;其次是 resource-id,一般也不会频繁变化;再往后是 xpath,可以用控件层级关系来定位;最后是坐标点,坐标最不稳定,设备分辨率一变就要全改。Appium Inspector 的 Search 功能可以直接测试你写好的表达式,比如通过 resource-id 找元素,选中后点击 “Tap” 按钮,设备上会真实触发一次点击,这是验证定位是否有效的快捷方式。
很多新手会陷入两个误区。一个是什么都想着用 xpath,xpath 表达式写得又长又脆弱,界面上一个节点顺序变化就挂了;另一个是过于依赖 text 文本,文本内容在国内的 App 里经常随着运营策略变化,用文字定位一旦文案改了,用例就翻了。我个人的习惯是:能用 content-desc 不用 resource-id,能用 resource-id 不用 xpath,坐标除非没有其他选项否则不用。
5. 编写可维护的测试脚本:从元素定位到业务用例
5.1 显式等待与隐式等待到底怎么选
移动端界面加载受网络和渲染影响很大,上一秒元素还不存在,下一秒才出来。此时如果脚本直接去查找,会抛NoSuchElementException。处理这类问题的标准方案是“等待元素就绪”,但很多项目跑挂了就是等待方式没用对。
Appium 客户端内置了三类等待方式。第一种叫隐式等待,通过driver.implicitly_wait(10)设置一个全局超时时间,在超时时间内反复尝试查找元素;第二种叫显式等待,通过WebDriverWait配合条件函数精确控制等待某个元素出现、可点击或消失;第三种是强制等待,也就是time.sleep(),虽然简单粗暴,但会让用例白等固定时间,网络快的时候也浪费。
我自己的经验是,隐式等待适合做一个兜底,比如设成 5 到 10 秒,但别把它当万能药,因为它对元素查找的每次重试都会有影响。显式等待是自动化用例里的主力,尤其是遇到登录、刷新这种耗时操作,等待“下一个页面标志性元素出现”比固定睡几秒更精准。我来举个例子,假设点击登录后,要等首页的“我的”标签出现,再继续操作:
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from appium.webdriver.common.appiumby import AppiumBy wait = WebDriverWait(driver, 10) profile_tab = wait.until( EC.presence_of_element_located( (AppiumBy.ID, "com.example.app:id/tab_profile") ) )这段话的意思就是:10 秒内,每隔一小段时间就去寻找tab_profile这个元素,找到了立刻继续,超时自动报错。用这种写法,用例跑得快的时候不用等满 10 秒,跑得慢了也能容错,测试时间的弹性一下就出来了。团队里我比较反对滥用time.sleep,因为它会把有效执行时间拉长,也不会让用例变得更可靠。
5.2 封装一个通用操作层,把细节收起来
项目里测试用例对应的操作,比如点击、输入、滑动、查找,如果每个用例都直接调 Appium 原生 API,代码会很散,而且定位策略一改,所有用例跟着遭殃。所以我会习惯性地把基础操作封装成一个类,让业务用例面向封装后的方法去编程。这样有几个很直接的好处:第一,你可以在统一的位置加日志,每个操作执行前后能输出设备和元素信息,排查问题时有迹可循;第二,可以在统一位置加失败截图处理,用例挂了自动留存截图,不再需要到处写 try-catch;第三,是“等待元素出现再执行操作”这类公共逻辑,只在封装处写一遍,所有用例自动继承。
举个例子,我把点击操作封装成这样:
class BasePage: def __init__(self, driver): self.driver = driver self.wait = WebDriverWait(driver, 10) def click_element(self, locator): element = self.wait.until(EC.presence_of_element_located(locator)) element.click() print(f"点击元素: {locator}")这里的locator可以是元组,比如(AppiumBy.ID, "com.example.app:id/btn_login")。通过这个封装,用例里只需要写page.click_element((AppiumBy.ID, "xxx"))。如果后续要调整等待策略、增加重试机制,所有的用例都能自动受益。封装不是越复杂越好,而是要把“测试过程中一定会重复做的事”抽出来,避免“复制粘贴式开发”。
5.3 pytest 组织用例与断言机制
pytest 是这套流程里的执行框架,负责扫描以test_开头的方法或函数,收集成测试用例,然后按顺序或规则执行,并给出通过、失败、错误等状态。移动端自动化测试用例放在 pytest 里,通常一个业务链路就是一个用例。比如“登录-进入首页-打开消息中心-断言消息列表加载完成”,就是一个端到端用例。pytest 里最简单的断言就是assert,比如判断某个文本是否出现在页面上:
def test_login_and_check_home(): driver = login_flow() home_text = driver.find_element(AppiumBy.ID, "com.example.app:id/home_title").text assert home_text == "今日推荐"这样写的断言虽然简单,但已经能解决大部分“页面有没有展示正确内容”的验证。如果是要验证列表数据条数、接口返回字段和界面一致性,可以在脚本里调用 App 的接口或者查数据库,不过这就超出了基础入门范围,这里不深入展开。
pytest 还有一个非常有用的功能叫 fixture,它可以承担“所有用例执行前的公共初始化”和“所有用例结束后的清理工作”。比如每个用例执行前都要连接设备、创建会话,结束后要关闭会话,就可以一次性写在 fixture 里。这样用例本身更干净,只保留业务操作和断言。我习惯把 fixture 放在conftest.py文件里,pytest 会自动发现如上配置。案例代码我贴一个小型框架样例,方便照着起步:
import pytest from appium import webdriver @pytest.fixture(scope="module") def driver(): caps = { "platformName": "Android", "appium:automationName": "UiAutomator2", "appium:deviceName": "emulator-5554", "appium:appPackage": "com.example.app", "appium:appActivity": ".MainActivity", "appium:noReset": True, } driver = webdriver.Remote("http://127.0.0.1:4723", caps) yield driver driver.quit()写完 fixture,在用例函数里只要加一个参数driver,框架会自动把创建好的会话实例传进去。通过这种方式,你可以在一个文件里写多个不同业务的测试用例,而不用每个用例从头启动一次 App。
5.4 测试报告输出与失败截图
测试结果不能只停留在命令行输出上,团队协作和问题定位都需要一份像样的报告。pytest 配合插件很容易生成 HTML 报告:在命令行执行pytest --html=report.html,跑完后会在当前目录生成一个包含用例名、执行时间、状态、失败日志的网页文件。如果你想在报告里看到每个步骤的截图,可以自己封装一个 pytest 钩子,在用例失败时调用 driver 的截图方法,保存图片并把图片路径追加到报告里。这个改造不复杂,但对团队的效率提升是直接的,否则测试挂了你还得重新跑一遍手工流程去复现。
Allure 是我更常用的一套方案,输出很美观,支持步骤层级、附件、历史趋势,适合做长期维护的测试集。接入方式稍微复杂一点,要装 allure-pytest 插件和 Allure 命令行工具,执行时参数也略有区别。不过我建议先把 pytest+HTML 跑顺,等用例量上到几百条时再考虑上 Allure,会更符合实际成长曲线。
6. 一个从启动到断言的完整案例拆解
很多文章喜欢把 Appium 的例子里只放“找到按钮并点击”。但真正的业务实践,往往是一个连续场景:启动 App、跳过多语言弹窗、登录、进入首页、滑动列表、进入详情、断言结果。我把这些串起来,写一个比较贴近真实项目的端到端用例。为了减少无关干扰,这里用一个购物类 App 的简化场景来演示,你完全可以照着这个思路替换成自己的业务。
我先定义页面对象,把“登录页”封装成一个类:
from appium.webdriver.common.appiumby import AppiumBy from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC class LoginPage: def __init__(self, driver): self.driver = driver self.wait = WebDriverWait(driver, 10) def input_username(self, text): el = self.wait.until(EC.presence_of_element_located( (AppiumBy.ID, "com.example.app:id/username"))) el.send_keys(text) def input_password(self, text): el = self.wait.until(EC.presence_of_element_located( (AppiumBy.ID, "com.example.app:id/password"))) el.send_keys(text) def tap_login_button(self): el = self.wait.until(EC.element_to_be_clickable( (AppiumBy.ID, "com.example.app:id/btn_login"))) el.click()然后写用例:
def test_login_flow(driver): login_page = LoginPage(driver) login_page.input_username("testuser") login_page.input_password("123456") # 收起键盘,避免键盘遮挡登录按钮 driver.hide_keyboard() login_page.tap_login_button() # 登录后进入首页,断言首页标志元素出现 home_title = WebDriverWait(driver, 15).until( EC.presence_of_element_located( (AppiumBy.ID, "com.example.app:id/home_title") ) ) assert home_title.text == "首页"这个用例看起来很短,但它已经覆盖了测试的核心:输入账号密码、收起键盘、点击登录、等待页面跳转、断言首页状态。你在跑这个脚本时,需要关注几个节点的表现。第一,在输入账号密码时,如果软键盘弹出后挡住了登录按钮,脚本点击会失效或者点到了错误的位置,所以要用 hide_keyboard 收起键盘。第二,如果 App 登录后有一个网络请求过程,等待时间设得太短会误报失败,这里设了 15 秒,如果网络慢可以再调大。第三,也是很多人容易漏掉的一点:断言不能只判断页面不崩,要判断“用户关心的价值是否达成”,比如首页标题文案是否正确。
实际操作时,你还可以把这类用例继续扩展。比如断言登录失败时出现错误提示文案,断言退出登录时回到登录页,断言登录态在杀掉 App 后依然保留。业务逻辑里的每一种路径,都可以拆解成一组这样的端到端用例。把这些用例集合到一个目录下,用 pytest 批量执行,一份基础版的移动端自动化测试集就成型了。
7. 常见问题与排查技巧实录
7.1 会话建立失败,错误日志看不懂
状态:执行脚本时报WebDriverException或者SessionNotCreatedException,日志甩了一大串,很容易吓到新手。其实解决办法很简单,第一眼看错误描述里的关键字段,比如An unknown server-side error occurred while processing the command.下面的Original error:部分,那里通常才是真正的原因。
我见过最多的情况有四种。第一种是 Appium Server 没启动干净,或者端口被占用了,可以关掉所有 Node 进程后重新启动 Appium。第二种是appPackage或appActivity写错了,App 进不了正确的页面就建立不了会话,你可以用adb shell里的包管理命令去核实应用信息。第三种是驱动没装,Appium 2.x 如果没装 uiautomator2 驱动,会直接报 “Please install the corresponding driver”,按提示补装即可。第四种是设备连接异常,会话建立前就发现deviceName与 adb 实际设备不匹配,建议检查 adb devices 输出。日志阅读上,别从上往下一整页硬看,先搜索 “Original error”,从它开始往后看,抓实质原因。
7.2 元素定位不到,控件明明在屏幕上
这个问题的排名能进 Appium 问题排行榜前三。元素在屏幕上显示,但脚本找它时被NoSuchElementException打断,原因可能是它还在加载中,也可能是它的属性和你以为的不一样。先排除加载问题:用显式等待,不要急着瞬间查找。再排除属性问题:打开 Appium Inspector 再看一眼它的 resource-id、content-desc、class,是不是和代码里的完全一致。
还有一类特殊场景,元素在层级里是存在的,但处于不可见状态或者上面遮了个弹窗,导致页面上看着有控件却点不了。这种情况建议先检查上方有没有权限弹窗、升级弹窗、活动浮标。在跑自动化之前,也可以用 Capabilities 里的autoGrantPermissions参数来自动授予 App 所需权限,避免弹窗干扰。如果 UI 控件是 WebView 渲染的,普通控件查找方式是找不到内部 HTML 节点,需要切换到 WebView 上下文再定位,这个就不在 Android 原生控件范围内了,属于进阶话题,但知道有这回事能帮你判断问题方向。
7.3 中文输入不进文本框
真机上用send_keys输入中文,不少同学遇到过内容上屏出错或者没输进去的情况。这个问题的根源通常不是 App 端,而是 Appium 使用的输入法不够完善。解决办法就是前面提过的两个 Capabilities 参数:
caps["appium:unicodeKeyboard"] = True caps["appium:resetKeyboard"] = TrueunicodeKeyboard启用 Appium 内置的 Unicode 输入法,resetKeyboard在会话结束后把输入法恢复成原来的设置。两个参数建议成对出现。如果你用的是模拟器,输入法问题相对少一些;真机上如果还是不行,可以在代码里先点击输入框,再直接粘贴文本到剪贴板,用driver.set_clipboard_text和长按粘贴的方式绕过去。这种做法虽然偏“技巧流”,但解决不了的时候很管用。
7.4 用例跑一长串后失败了,怎么定位是哪一步出问题
移动端测试跑批量和单个用例的感觉很不一样,单个跑得好好的,串起来跑就会出现各种依赖问题。最常见的是用例间状态残留,比如上一个用例没有退出登录,下一个用例却假设从登录页开始。解决方法是依赖 fixture 做清理,比如用例结束之后执行driver.terminate_app(package)或点击退出登录。如果你发现失败用例总是和特定顺序有关,优先怀疑用例间互相干扰,而不是脚本本身的业务逻辑问题。
排查定位还有一个非常高效的手段:在关键步骤加日志和截图。我自己的习惯是,在每次点击和断言前都打印当前页面标题、页面源码片段,并且至少在失败时自动截图。pytest 里可以通过request.node拿当前用例名,然后把截图存到以用例名命名的文件里。这样跑完一轮,直接看失败用例的截图,基本能判断是弹窗遮挡、文案变化还是流程跳转异常。平时看日志不要只看成功还是失败,“在哪个页面、点了什么、发生了什么操作”才是最快定位的关键信息。
7.5 一张速查表帮你收敛常见的异常
| 异常现象 | 常见原因 | 处理建议 |
|---|---|---|
| 会话建立失败 | 端口占用、驱动缺失、包名/Activity 错误 | 查 Original error,按提示补驱动、核实包名 |
| 找不到元素 | 加载延迟、属性错误、弹窗遮挡 | 用显式等待,重新打开 Inspector 核实属性 |
| 输入中文失败 | 输入法不支持、键盘未收起 | 启用 unicodeKeyboard/resetKeyboard |
| 点击位置偏移 | 软键盘弹出、分辨率不同 | 输入后收起键盘,控件优先用布局定位 |
| 跑批偶发失败 | 用例间状态残留、网络超时 | 完善 fixture 清理逻辑,关键操作加重试机制 |
| 设备离线 | USB 线问题、调试授权失效 | 换线、重新插拔、在手机上重新授权 |
8. 关于持续集成与后续扩展的个人体会
到这里,一条完整的 Appium 自动化测试链路已经从环境、脚本、定位、运行到问题排查都过了一遍。如果你把上面的代码样例照着自己项目改一改,能跑通第一条用例,这个东西就算入了门。可是要把它真正用在工作里产生价值,我建议你马上想一件事:怎么让它每天自动跑起来。
我自己团队里的做法是接 Jenkins。把测试代码提交到代码仓库后,在 Jenkins 上配置一个任务,定时拉取最新代码,执行 pytest 命令,生成 Allure 报告并在构建结果里展示。设备可以选择宿主机连接的 Android 真机,也可以用 Docker 里的模拟器方案,不过模拟器的可维护性通常不如一台长期通电的真机稳定。如果资金和网络条件允许,也可以接入云测平台,把用例跑在远程设备池上,一次性覆盖多种机型,但脚本要提前处理好设备分辨率差异和系统版本差异。
还有一点想单独提一下,就是测试脚本本身的“代码质量”和业务代码一样需要维护。元素定位的变化、页面流程的调整、测试数据的过期,都会让用例翻车。我见过很多团队一开始搭自动化跑得很欢,半年后没人维护,用例挂了一堆也不修,最后整套体系废弃,非常可惜。自动化测试的价值不是“搭出来”,而是“持续稳定地提供回归信号”。所以每次 App 版本更新,测试代码的评审和更新应该跟业务代码同期进行,而不是等用例挂了再救火。
关于 Appium 的未来扩展方向,比较实际的有三条路:一是把 Appium 用例和接口自动化测试结合,在登录、数据准备这些环节调用真实接口完成前置条件,能省不少时间;二是引入图像识别作为兜底断言,比如用 OpenCV 比对界面截图,弥补控件属性缺失的场景;三是基于 AI 的方式让工具自动生成一些基础的 UI 操作脚本,这条路目前还在快速迭代里,不建议还没把基础功底打牢就追这个风口。说到底,工具只是杠杆,真正决定自动化项目成败的,是你对业务的理解和对测试工程化的把控。
最后分享一点实操心得。刚开始学 Appium 的时候,别一上来就追求搭一个完美的框架,先把最简单的一条用例从手工操作变成脚本跑通,再逐步加等待、封装、断言、报告、批量执行,这样每一步的反馈都很及时,也知道每一步解决了什么问题。移动端自动化这条路很宽,但它的第一步从来都不难,就藏在你第一次成功跑起来的那个会话里。