1. 为什么要选Appium:聊聊我的入坑原因
这两年移动端测试的活越来越重,手工点来点去不仅效率低,版本迭代一快就完全跟不上。我自己在测试开发这条路上摸爬滚打几年,先后试过不少工具,最后真正让我定下心来深耕的,还是Appium。如果你正在接触移动应用自动化测试,或者想把手里的测试工作系统化地做起来,Appium几乎是个绕不开的选项。
先说清楚Appium是什么。它是一个开源的移动端自动化测试框架,支持iOS、Android、Windows等平台,核心特点是"一套代码多端复用"——通过WebDriver协议与手机交互,把你在电脑上写好的测试指令翻译成手机能听懂的操作。相比其他工具,Appium的优势在于:不依赖手机端安装任何额外Agent,用的是系统自带的自动化引擎(iOS的XCUITest、Android的UiAutomator2),这意味着你测的是用户真正会拿到的那个App,而不是某个被注入过的特殊版本。
我在实际工作中选Appium,还有几个很实在的理由。第一,它跨语言,Python、Java、Ruby、JS都能写,团队用什么栈都能接上;第二,它生态成熟,遇到问题随便一搜就有解决方案;第三,它支持真机、模拟器、云测平台,灵活度很高。当然它也不是没有槽点,比如环境配置确实烦,慢的时候让人抓狂,但这些后面我会一一讲怎么解决。
这篇文章适合谁看?如果你刚接触移动应用自动化测试,或者写了几条用例但总感觉不太稳固,想系统地把环境、定位、脚本、调优整条链路都摸明白,那这篇文章就是给你准备的。我会从环境搭建开始,一步步走到编写能跑的测试脚本,最后把那些"文档里查不到"的坑也一并倒出来。
2. 环境搭建的每一步都给你捋清楚
2.1 先装什么、后装什么,顺序很重要
Appium的环境搭建是入门的第一道坎,很多人就是倒在这一步的。我见过太多同事卡在"明明照着教程装的,为什么就是跑不起来",大部分原因其实是安装顺序和版本匹配的问题。我的建议是先装依赖,再装Appium本体,最后配置手机和模拟器。
首先是Java。虽然我们可以用Python写测试脚本,但Appium的Android部分依赖Java环境来驱动UiAutomator2,所以JDK必须装。我推荐装JDK 8或11,太新的版本有时候反而会跟旧的SDK组件产生兼容问题。装完后在命令行执行java -version确认能正常输出版本信息。
接下来是Android SDK。如果你只测iOS,那可以跳过这步,但绝大多数入门场景都是从Android开始的。SDK不一定要装Android Studio全家桶,但至少需要有adb、build-tools、platform-tools这几个部分。装好后把ANDROID_HOME环境变量配好,指向你的SDK目录。
然后是Node.js。Appium Server本身是Node写的,所以Node环境必须准备好。这里有个小建议:Node版本不要追新,装LTS长期支持版就好,我遇到过在太新的Node版本上装Appium报错的情况。
以上都准备好了,就可以用npm来安装Appium本体了。现在官方推荐的是Appium 2.x版本,跟早期的1.x在插件管理上有一些区别。安装命令很简单:
npm install -g appium装完执行appium --version,能输出版本号就说明Server本体OK了。但别急着高兴,Appium 2.x默认不包含各个平台的驱动,你还得单独安装UiAutomator2驱动:
appium driver install uiautomator2这一步在国内网络环境下经常超时,如果遇到失败,可以配置镜像源再试。
2.2 为什么建议装Appium Inspector而不是只用代码定位
环境装好之后,很多人会直接开始写脚本,这是新手最容易走弯路的地方。你连页面上那个按钮长什么样、id是什么都不知道,怎么写定位?所以我强烈建议安装Appium Inspector——它是Appium官方提供的元素检查工具,能直接连接到你的手机或模拟器,把当前页面的UI层级结构像DOM一样展示出来。
有了Inspector,你可以直接查到元素的id、class、xpath、accessibility id等定位信息,还能实时验证你的定位表达式能不能找到元素。这比一边写代码一边猜靠谱得多。Appium Inspector一般通过Appium Desktop安装,或者在Appium 2.x里通过插件方式使用。
注意:Inspector连接手机之前,手机必须已经开启USB调试,并且当前页面的App处于激活状态。模拟器的话要确保已经启动且未被其他adb进程占用。
我用Inspector的日常流程是这样的:先启动Appium Server,再打开Inspector,填好Desired Capabilities(设备名、平台版本、App包名等信息),点击启动会话,就能看到手机屏幕的实时映射和UI树了。查到一个元素后直接复制它的定位信息,回到代码里用,效率很高。
3. Desired Capabilities:跟你手机正确"握手"的暗号
3.1 核心参数逐一解释
Desired Capabilities可以理解成一份"连接说明",告诉Appium你要测什么设备、什么系统、什么App。这些参数要是写错了,后面什么都白搭。我先把我最常用的一组参数拿出来,并逐个说明含义:
caps = { "platformName": "Android", "appium:deviceName": "emulator-5554", "appium:platformVersion": "12.0", "appium:app": "/path/to/app.apk", "appium:automationName": "UiAutomator2", "appium:noReset": True, "appium:unicodeKeyboard": True, "appium:resetKeyboard": True }platformName就是平台类型,Android或iOS,这个没什么悬念。deviceName看着是设备名,实际上只要你保证adb能识别到设备,这个值写什么影响不大,我一般直接写emulator-5554或设备型号字符串。platformVersion就是Android版本,注意不是手机的销售版本号,而是系统设置里那个"Android 版本"的数值。app指向APK文件的绝对路径,如果你已经安装了App,也可以不传这个参数,改用appPackage和appActivity来定位启动的界面。
3.2 参数选错会遇到的典型坑
automationName是我反复强调的参数。在Appium 2.x里,Android平台默认就是UiAutomator2,但如果你在跑老项目,用了Appium 1.x的遗留代码,不写这个参数时默认值可能是旧的UiAutomator,导致一些新的API不可用或者定位方式不兼容。所以我的习惯是永远显式写上。
noReset也很关键。它表示在测试前后不要重置App的数据。如果你测的是需要登录的App,不写这个参数或者设为False,每次启动都会回到初始状态,登录状态就没了,用例也就没法跑。我一般设为True,让App保持我之前手动设置好的状态。
unicodeKeyboard和resetKeyboard是用来解决中文输入问题的。模拟器默认的输入法可能不支持sendKeys输入中文,这两个参数配合上adb shell ime set切换输入法,才能稳定地往输入框里填中文。还有些细节,比如测试iPad时有个udid参数,Web测试时有browserName参数,场景不同会有差异,但你可以遇到一个学一个,核心那几个先记住就够入门了。
4. 元素定位:Appium Inspector拿到的那几个信息到底怎么用
4.1 五种最常用定位方式对比
元素定位是自动化测试的核心技术活。你定位的稳定性,直接决定了测试脚本能不能长期可靠地跑下去。用Appium Inspector能拿到的定位信息主要有五种:id、class、xpath、accessibility id、text(文本内容)。
在Android上,资源的id是最优先的选择,格式类似com.example.app:id/btn_login。它本身就是开发者给UI控件起的"身份证号",只要开发不改ID,这条定位就是最稳的。XPath则是最灵活的,你可以通过任意属性值组合来定位,比如"找一个文本为'登录'的Button",但灵活也意味着脆弱,UI一调整层级可能就挂了。accessibility id在Android上往往对应content-desc属性,专门给无障碍功能用的,如果你测的App无障碍做得规范,这个定位方式也相当稳定。
我把它们的稳定性、灵活性和适用场景整理成了一张对比表,方便你参考:
| 定位方式 | 稳定性 | 灵活性 | 适用场景 |
|---|---|---|---|
| id | 高 | 低 | 首选,UI结构变化不影响 |
| accessibility id | 高 | 低 | App有完善的无障碍属性时 |
| class | 中 | 低 | 同类型控件少时可用 |
| xpath | 中高 | 高 | 组合条件定位,但结构变更容易失效 |
| text | 中 | 中 | 文本唯一且页面简洁时 |
4.2 怎么写出健壮的定位表达式
关于XPath,很多新手听到就头疼,其实不用怕。核心就一个思路:从你能唯一辨认的属性入手,组合起来。别去复制Inspector自动生成的那长串绝对路径,那种写法几乎只有当下的UI结构能用,稍微改一个层级就全废。我更推荐写相对定位,比如:
el = driver.find_element("xpath", "//android.widget.Button[@text='登录']")这句话的意思是:在整个页面里找文本内容为"登录"的按钮。这种写法没有死板的层级依赖,就算这个按钮往上挪了一层,依然能找到。还有一个实用技巧是组合索引,比如页面上有多个同名的控件,可以这样写:
el = driver.find_element("xpath", "(//android.widget.TextView[@text='热门'])[1]")先定位所有文本为"热门"的TextView,再取第一个。这里要注意索引从1开始,不是0,跟代码数组下标不一样,这也是我刚入门时踩过的坑。
4.3 等元素出现的正确姿势
定位写好了,不能马上操作。App页面加载是有延时的,尤其是网络请求回来之后列表才渲染完。你一启动就急着找元素,大概率迎头一个"Element Not Found"。我的做法是加显式等待,等某个元素出现后再继续:
from appium.webdriver.common.appiumby import AppiumBy from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC wait = WebDriverWait(driver, 15) login_btn = wait.until( EC.presence_of_element_located((AppiumBy.ID, "com.example.app:id/btn_login")) ) login_btn.click()WebDriverWait会每500毫秒检查一次元素是否出现,最长等15秒,超时就抛出异常。这就避免了盲目time.sleep()浪费时间或者等待不足导致随机失败。等待策略是整个测试稳定性的一个分水岭,我后面会专门展开讲。
5. 从零写一个能跑的登录测试脚本
5.1 项目结构怎么组织才不混乱
到了这一步,你环境OK了,定位信息也拿到了,该写第一个真正的测试脚本了。我的建议是别一上来就写一个几百行的大文件,先把结构搭好,用pytest作为测试框架来组织。下面是我推荐的一个最小但完整的项目结构:
test_app/ ├── conftest.py # pytest的全局fixture ├── config.py # 设备配置和App路径 ├── pages/ # 页面对象,一个页面一个类 │ ├── login_page.py │ └── home_page.py ├── tests/ # 测试用例 │ └── test_login.py └── requirements.txt很多新手觉得这样分太复杂,但等用例规模上去了,你就知道好处了。页面对象模式(Page Object Model,简称POM)的核心思想是:把每个页面的元素定位和操作方法封装在独立的类里,测试用例本身只关心业务逻辑。比如登录页的login_page.py负责找到用户名输入框、密码输入框、登录按钮,并提供login(username, password)方法;测试用例只需要调用这个方法。哪天登录页UI改版了,你只需要改LoginPage一个文件,测试用例一行不用动。
5.2 关键的登录测试用例一步一步写出来
我们以经典的登录场景为例,完整写一遍。先看conftest.py里的Driver管理。pytest的fixture机制非常方便,我一般这么写:
import pytest from appium import webdriver from config import caps @pytest.fixture(scope="class") def driver(): driver = webdriver.Remote("http://127.0.0.1:4723/wd/hub", caps) driver.implicitly_wait(10) yield driver driver.quit()这个fixture会在测试类运行前启动一个连接,所有测试方法共用这个driver,用完自动退出。这里的implicitly_wait(10)是设了一个全局的隐式等待:每次找元素时,如果找不到会在10秒内不断重试。隐式等待和显式等待可以搭配使用,但注意不要叠加过大的数值,否则定位失败的用例会拖得很久。
登录页的页面对象可以这样写:
from appium.webdriver.common.appiumby import AppiumBy class LoginPage: def __init__(self, driver): self.driver = driver def input_username(self, text): el = self.driver.find_element(AppiumBy.ID, "com.example.app:id/username") el.send_keys(text) def input_password(self, text): el = self.driver.find_element(AppiumBy.ID, "com.example.app:id/password") el.send_keys(text) def click_login(self): self.driver.find_element(AppiumBy.ID, "com.example.app:id/btn_login").click() def login(self, username, password): self.input_username(username) self.input_password(password) self.click_login()最后是测试用例本身,看整体怎么串起来:
import pytest from pages.login_page import LoginPage from pages.home_page import HomePage class TestLogin: def test_login_success(self, driver): login_page = LoginPage(driver) login_page.login("tester@example.com", "123456") home_page = HomePage(driver) assert home_page.is_logged_in(), "登录成功后应跳转到首页"这里HomePage里需要实现一个is_logged_in()方法,判断首页的一个特征元素是否存在。用断言来验证结果,这就是自动化测试的骨架——操作、验证,一步都不能少。有些人写完操作步骤就结束了,不做断言,那顶多叫脚本,不叫测试。
5.3 pytest命令行跑起来以及结果怎么看
在项目根目录执行:
pytest -v tests/test_login.py-v参数会输出每个用例的执行细节。如果一切顺利,你会看到一条PASSED。如果你想把结果输出成HTML报告,可以用pytest-html插件:
pytest -v tests/test_login.py --html=report.html生成的报告里会有每个用例的执行时间、失败时的完整错误栈,还有截图功能可以配置。我通常会配合--maxfail=1参数,用例失败太多时及时停住,省得浪费时间:
pytest -v tests/test_login.py --html=report.html --maxfail=16. 常见问题与排查技巧实录
6.1 会话就是启动不了
这是环境搭建后第一道拦路虎。现象通常是执行脚本时报Could not create a new session或者An unknown server-side error occurred。我的排查步骤是固定的:先命令行里执行adb devices,确认设备在列表里,状态是device而不是offline;然后执行adb shell echo ping,确认设备响应正常;最后看Appium Server的日志,里面通常有详细的错误原因。一个高频原因是appPackage和appActivity写错了,App包名和启动Activity可以用下面的命令获取:
adb shell pm list packages | grep -i 你的应用关键词 adb shell dumpsys window | grep mCurrentFocus第二条命令能拿到当前前台运行的Activity名称,非常实用。
6.2 元素定位间接性失败
这是最磨人的问题。脚本今天跑得好好的,明天一跑就提示找不到元素了。多半是元素等待不够,或者是某个动态内容延迟加载。我的经验是:
- 先区分是完全没有这个元素,还是元素出现过但被遮挡、不可点击;
- 优先用显式等待替代固定的
sleep; - 如果元素偶尔可见但点不到,试一下
driver.tap()配合坐标或者先滚动屏幕让它真正进入可视区域。
我曾经遇到一个列表页,前几次启动时数据加载快,元素很快出现,后面因为缓存状态变化,加载变慢,用例就开始翻车。后来我把所有关键路径都改成显式等待,随机失败率从三成降到了接近零。
6.3 中文输入乱码怎么回事
在Android模拟器上,使用send_keys输入中文经常遇到乱码或者压根输入不进去。这个问题我在2.3节提到的两个Capabilities参数里已经埋了伏笔。除了把unicodeKeyboard和resetKeyboard都设为True,你还需要确保系统当前有可用的中文输入法。有些精简过的模拟器镜像不带中文输入法,那就在设置里加一个谷歌拼音或者搜狗输入法。设置完输入法后重启测试会话再试,基本能解决。
6.4 定位表达式写了但是报语法错误
XPath表达式写错是考试里最常见的低级错误。报错信息里提示invalid xpath时,优先检查引号配对和路径首字符。XPath基础规则就三条://表示相对路径查找,[@属性名='值']表示属性筛选,text()='文本'表示文本匹配。括号要注意索引写法是(表达式)[N]而不是表达式[N],这个我前面也强调过,新人在这里栽跟头非常多。
7. Appium实际落地时的几个心得
跑通第一条用例之后,很多人会觉得自己入门了,但离"能稳定落地"还有距离。我在团队里推行Appium的过程中,总结了几条实际心得。
真机还是模拟器,别纠结太久。真机更接近用户真实环境,但充电、锁屏、网络状态都会影响稳定性;模拟器速度快、环境可控,适合大批量回归。我的建议是日常开发用模拟器,关键版本发布前在真机上跑一遍冒烟测试。另外,云测平台也能跑Appium用例,适合需要覆盖大量机型矩阵的场景,代价是调试时看不到实时日志,排错效率低。
用例的独立性是所有落地经验里最重要的一条。每个测试方法执行前都要确保App处于预期初始状态,不要让上一条用例的状态污染下一条。之前团队有人写了一条"创建订单"的用例,没有清理数据,结果后面所有依赖订单列表的用例全部失败。我后来在fixture里加了重置逻辑,每个用例跑完自动清数据,整个测试集的稳定性一下就上来了。
还有一点是日志。别等到报错了才去翻日志,要在写用例的时候就养成打印关键步骤的习惯。用Python的logging模块,每找到元素、每点击一下,都打一条带时间戳的日志。出了问题之后,把日志从头翻一遍,定位失败的那一步往往一目了然,比盯着报错信息瞎猜快得多。我在实际调试中,十条问题有九条是靠着完整日志定位出来的。
Appium入门这件事,说穿了就三步:环境跑通、定位熟练、结构清晰。你把这三点啃下来,剩下的就交给时间和项目去打磨了。我现在回头看我当时踩过的一个个坑,其实都是必经之路,只是当时要是有人把这些坑提前跟我讲清楚,能少走很多弯路。希望这篇文章能成为你路上的那块垫脚石。