很多做移动端测试的同学,第一次接触 App 自动化时会发现:网上资料很多,但版本混杂,照着旧教程做完,Appium Inspector 连不上手机,Python 脚本跑起来一堆报错,最后连一个真实的业务流程都没跑通。如果你最近正在了解 Python + Appium 这套技术栈,打算给真实 App 做 UI 自动化测试,那么今天这篇文章会比较适合你。
文章会用网易严选 App 作为被测对象,从环境搭建、核心原理、元素定位,到 Page Object 封装、pytest 用例执行和问题排查,完整走一遍企业级 APP 自动化测试的落地流程。阅读时可以跟着步骤边配置边验证。掌握这套流程后,换到商城、资讯、工具类 App,核心思路基本都能迁移使用。
1. 背景与核心概念
1.1 Appium 到底解决什么问题
App 自动化测试的核心痛点,是“重复操作太多”。传统手工回归测试里,每次版本发布前,测试人员都要在手机上反复执行启动、搜索、下单、退出等流程,工作量巨大,而且容易漏测。Appium 解决的就是这个重复执行问题:它允许我们通过 Python、Java、JavaScript 等语言编写脚本,像真实用户一样操控 Android 或 iOS 设备上的 App。
和很多自动化框架不同,Appium 不是针对某一个 App 写死的脚本库,而是一个通用测试服务。你可以理解成它给手机提供了一个“遥控中心”,测试代码通过 HTTP 请求告诉 Appium:启动哪个应用、点击哪个控件、输入什么文字、断言页面是否出现。正因为这种通用性,Appium 能适配不同 App,企业里也常把它和 pytest、Jenkins 等工具结合,形成夜间自动化回归体系。
对于 Python 技术栈的测试团队来说,Appium 的优势还在于语言生态。Python 能做数据构造、接口调用、测试报告生成,再配合 pytest 断言体系,很容易把 UI 自动化用例纳入一套完整的质量保障流程中。
1.2 Appium 自动化测试的工作方式
Appium 不是直接在手机上运行 Python 脚本,它底层采用 Client-Server 架构。测试代码里的 Python 是客户端,通过 WebDriver 协议把操作命令发送给 Appium Server,Appium Server 再把命令转换成对应移动平台的原生指令。
以 Android 为例,常见的驱动是 UiAutomator2。Appium Server 收到点击命令后,会交给 UiAutomator2 Server 去执行元素查找、点击、输入等操作。iOS 端则使用 XCUITest 驱动完成类似事情。这套设计让测试用例和底层设备实现解耦,写脚本时基本不用关心手机系统如何执行点击,只需要关心业务元素。
这里有个容易混淆的点:Appium 2.x 与旧版 Appium 1.x 的驱动安装方式不一样。1.x 时代,UiAutomator2 等功能往往内置在 Appium Desktop 安装包里;Appium 2.x 之后,Server 和 Driver 分离了,需要按驱动单独安装。现在学习建议直接使用 Appium 2.x 的稳定版本,下面章节也会按照 2.x 的思路来准备环境。
1.3 企业级 App 自动化测试的分层思路
刚接触自动化时,很多人以为“能跑一条脚本”就是自动化完成了。到真实项目里,企业级 App 自动化通常不是单条脚本,而是一个分层体系:
| 分层 | 作用 | 常见技术 |
|---|---|---|
| 用例层 | 描述业务流程和断言 | pytest、unittest |
| 页面对象层 | 把页面元素和操作封装成类 | Page Object 模式 |
| 驱动与能力层 | 管理设备、Appium Server、Desired Capabilities | Appium Client |
| 基础设施层 | 设备管理、报告、失败重跑、CI 触发 | pytest-html、Allure、Jenkins |
本文后面的实战会覆盖用例层、页面对象层和驱动能力层,基础设施层会给出建议和扩展方向。这样即使被测 App 变化,维护成本也能控制在可接受范围。
2. 没有基础,如何 3 天上手
“3 天搞定企业级 APP 自动化”并不是说 3 天能成为自动化测试专家,而是指按合理路线,3 天足够从零搭建一套能够跑通真实业务用例的工程。如果之前只会 Python 基础语法,没写过移动自动化,可以按下面的节奏执行。
2.1 Day1:跑通环境并启动网易严选 App
第一天不追求写复杂逻辑,重点是把环境跑通。需要完成 Python 安装、Appium Server 安装、Android 设备连接、网易严选 App 安装,最终通过脚本启动网易严选并打印当前页面信息。
这一天最容易失败的是环境问题,比如 adb 连接不到手机、Appium Inspector 配置错误、Desired Capabilities 不完整。建议遇到一个解决一个,并记录到自己的笔记里。环境通了,后面学习才会顺畅。
2.2 Day2:理解元素定位并完成搜索主流程
第二天开始理解 UI 自动化怎么找控件。打开 Appium Inspector,像查看微信开发者工具一样查看网易严选界面元素,弄清 resource-id、text、content-desc、xpath 之间的关系。然后编写搜索流程:启动 App、点击搜索框、输入保温杯、点击搜索结果、进入商品详情页。
建议不要把目标定得太宽。搜索并进入详情页,已经覆盖启动、点击、输入、断言四个核心动作。学会这条链路,注册、登录、下单等流程基本就是重复同样套路。
2.3 Day3:走向工程化
第三天做工程化改造:把重复代码封装成页面对象类,加入显示等待,补充失败截图,接入 pytest 生成测试报告,并处理常见的无法定位、输入不生效问题。
3 天结束后,应该拥有一个可以直接运行的 Python 自动化项目,而不是一堆粘贴在临时 py 文件里的脚本。
3. 环境准备
3.1 Python、编辑器与依赖
如果在 Windows 上学习,先从 Python 官网下载稳定版本安装包。安装时务必勾选“Add Python to PATH”,否则后面在 cmd 中运行 python 命令会提示找不到。macOS 上可直接通过 Homebrew 安装,Linux 则可以按发行版使用包管理器或源码编译。
安装完成后,在终端里执行:
python --version pip --version如果两个命令都能正常输出版本号,说明 Python 环境没问题。编辑器推荐使用 VS Code 或 PyCharm。VS Code 里需要安装 Python 扩展,按 Ctrl+Shift+P 输入 Python: Select Interpreter,选择当前 Python 环境;PyCharm 则在 Settings 中新建虚拟环境即可。
后面创建自动化项目时,建议使用 venv 或 conda 虚拟环境,避免多个项目依赖互相冲突。本文示例用的是 pip + venv 思路。
3.2 安装 Appium Server 与 Appium Inspector
Appium 2.x 的 Server 基于 Node.js 运行,所以电脑上需要先安装 Node.js。安装后打开终端执行:
npm install -g appium appium --version如果输出了 Appium 版本号,说明安装成功。第二步安装 Android 驱动:
appium driver install uiautomator2执行appium driver list可以查看当前已经安装了哪些驱动。Appium Inspector 是独立的可视化元素定位工具,可以把它理解成移动端自动化的“放大镜”,通过它查看当前界面的控件树和属性。可以从官方 GitHub Releases 页面下载对应操作系统的安装包,安装后打开。
启动 Appium Server,在终端执行appium,如果看到 Appium 服务跑在 4723 端口,就说明 Server 已经就绪。Appium Inspector 默认连接地址也是127.0.0.1:4723,很少需要修改。
3.3 Android 设备准备与获取包名信息
Android 自动化测试既可以连接真机,也可以使用模拟器。真机测试需要在手机中开启开发者选项和 USB 调试,并通过数据线连接电脑。连接后执行:
adb devices如果设备列表里显示device状态,说明 adb 识别正常。接下来需要确认被测应用是否安装。网易严选的包名通常是com.netease.yanxuan,不同渠道版本可能不同,可以通过如下命令过滤:
adb shell pm list packages | grep yanxuan拿到包名后,再获取当前界面 Activity。先在手机上手动打开网易严选,回到终端执行:
adb shell dumpsys activity activities | grep -E 'mResumedActivity|mFocusedApp'输出会包含类似包名/Activity名的信息。由于 Android 版本不同,命令输出格式会有差异,但思路是一样的:确认包名、确认启动 Activity,再把这两个值填到自动化配置里。如果实在无法从命令中解析,也可以先随便填写,通过 Appium Inspector 连接后查看启动日志,日志里通常会显示 Appium 实际识别的 Activity。
4. 核心配置与必须掌握的原理
4.1 Desired Capabilities:告诉 Appium 要操作什么设备
Desired Capabilities 是 JSON 格式的配置项,用于描述本次自动化会话的属性。它可以告诉 Appium 当前是 Android 还是 iOS、设备名称、应用包名、Activity、是否保留登录状态等。常见的配置如下:
caps = { "platformName": "Android", "appium:automationName": "UiAutomator2", "appium:deviceName": "Android Emulator", "appium:platformVersion": "13", "appium:appPackage": "com.netease.yanxuan", "appium:appActivity": "MainActivity", "appium:noReset": True, "appium:unicodeKeyboard": True, "appium:resetKeyboard": True, }这里的appium:appPackage和appium:appActivity要与上一步从设备上获取的值保持一致。noReset为 True 时,Appium 不会在用例结束后清除 App 数据,能避免每次都要重新登录或重新初始化;但如果用例之间互相依赖登录状态,反而可能掩盖问题,实际项目中需要按团队规范决定。
4.2 元素定位:为什么不能只复制 XPath
UI 自动化最关键的操作是“找到按钮并点击”。Appium Inspector 打开网易严选首页后,会显示当前界面树。左侧是控件列表,右侧是被选元素的属性。定位方式通常有 resource-id、text、content-desc、class name、xpath 等。
尽量优先使用 resource-id 或 text 组合。不要看到 xpath 顺手复制,因为页面顶部控件层级一变,长 xpath 很容易失效。比如一个常见的写法是:
old_xpath = "/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout"这种路径一旦页面加了新布局就挂。更建议用相对可靠的 text 或 class 组合:
(AppiumBy.ANDROID_UIAUTOMATOR, 'new UiSelector().text("搜索")')或者退而求其次,使用尽量短的 xpath:
//android.widget.TextView[contains(@text,"加入购物车")]实际定位复杂页面时,可以先在 Appium Inspector 中点击需要的控件,重点查看元素的resource-id、text、content-desc。当这几个属性都为空时,才使用控件层级关系。
4.3 显示等待与隐式等待
App 页面加载存在网络延迟,元素并不是启动后立即可见。如果脚本像人一样不等待直接点击,大部分情况会报NoSuchElementException。
初学者常见的做法是time.sleep(5),这种固定等待不适合工程化:网络慢时 5 秒不够,网络快时白白等 5 秒。工程中更推荐使用 Selenium 的WebDriverWait显式等待,它会在规定时间内不断轮询,直到元素满足条件:
WebDriverWait(driver, 20).until( EC.presence_of_element_located(locator) )WebDriverWait里还可以配合visibility_of_element_located、element_to_be_clickable等条件使用。后面项目实战代码里会反复见到。
4.4 send_keys 输入失效的常见原因
在 App 输入框里输入文字时,不少同学会遇到两个问题:第一是点击了输入框但光标没有出现;第二是 Python 脚本里 send_keys 中文,手机上显示乱码或者完全不显示。
第一个问题常见于键盘没有弹起,这时可以在输入前用driver.tap()点一下输入框区域,或者使用 UiAutomator2 的focus能力。第二个问题通常和设备默认输入法有关,这也是 Capabilities 中配置unicodeKeyboard与resetKeyboard的原因。前者让 Appium 使用 Unicode 输入方式,后者在自动化结束后把键盘重置为设备默认输入法。
input_el = wait.until( EC.visibility_of_element_located(search_input_locator) ) input_el.click() input_el.send_keys("保温杯")如果搜索结果页面没有“搜索”按钮可点击,也可以在输入完成后发送 Android 回车键触发搜索:
driver.press_keycode(66)不同品牌手机输入法对回车键的响应不同,但 66 这个按键码在绝大多数原生输入框里都表示 ENTER,很多搜索场景都可以先试这一种。
5. 网易严选实战:从搜索到进入商品详情
5.1 被测流程说明
本文执行的业务链路是:启动网易严选 App → 在首页点击搜索入口 → 输入商品关键词“保温杯” → 触发搜索 → 点击第一个搜索结果 → 等待进入商品详情页并断言“加入购物车”按钮存在。
选择这条链路,是因为它不涉及登录、支付等强账号体系,测试执行更稳定。真实企业中如果要下单、支付,往往要另外准备测试账号和预置商品,而且会涉及更复杂的账号信息安全边界。示例不点击真实购买按钮,只到详情页断言层级,既能覆盖核心 UI 自动化操作,又能避免测试数据污染线上订单系统。
5.2 项目结构
我会用 Page Object Model,也就是页面对象模式来组织代码。把每个页面的元素定位和操作封装成独立类,测试用例只关心业务流程,而不是散落的 find_element。
项目结构如下:
yanxuan_appium_demo/ ├── requirements.txt ├── caps.py ├── pages/ │ ├── __init__.py │ ├── base_page.py │ ├── home_page.py │ ├── search_page.py │ └── goods_page.py └── tests/ ├── __init__.py └── test_yx_search.py这样可以做到:网易严选首页布局变了,只修改home_page.py;搜索逻辑变了,只修改search_page.py;测试用例本身尽量保持稳定。
5.3 创建依赖文件与基础配置
先在项目根目录创建requirements.txt:
Appium-Python-Client pytest pytest-html selenium版本不写死的好处是安装时自动找当前兼容版本。如果你希望追求稳定复现,也可以把版本固定下来。然后创建虚拟环境并安装依赖:
python -m venv venv venv/Scripts/activate pip install -r requirements.txtmacOS/Linux 下激活虚拟环境的命令是source venv/bin/activate。安装完成后,编写caps.py:
# 文件路径:caps.py def get_caps(): return { "platformName": "Android", "appium:automationName": "UiAutomator2", # 真机或模拟器设备名,Appium 2.x 中不能为空 "appium:deviceName": "Android", # 按当前设备实际 Android 版本填写 "appium:platformVersion": "13", # 通过 adb shell pm list packages 确认 "appium:appPackage": "com.netease.yanxuan", # 通过 adb shell dumpsys activity 获取 "appium:appActivity": "MainActivity", "appium:noReset": True, "appium:unicodeKeyboard": True, "appium:resetKeyboard": True, # 超过 300 秒未收到命令自动关闭会话 "appium:newCommandTimeout": 300, }这里要特别强调:appPackage与appActivity不要照抄。不同版本、不同渠道的网易严选安装包,启动 Activity 可能存在差异。先按 3.3 节的方法从设备拿到正确值,再回填到配置中。
5.4 封装 BasePage
BasePage是所有页面对象的父类,统一封装查找、点击、截图、等待逻辑。这样后续写 HomePage、SearchPage 时,代码会干净很多。
# 文件路径:pages/base_page.py from appium.webdriver.common.appiumby import AppiumBy from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC class BasePage: def __init__(self, driver): self.driver = driver def wait_visible(self, locator, timeout=15): return WebDriverWait(self.driver, timeout).until( EC.visibility_of_element_located(locator) ) def click(self, locator, timeout=15): element = WebDriverWait(self.driver, timeout).until( EC.element_to_be_clickable(locator) ) element.click() def send_keys(self, locator, text, timeout=15): element = self.wait_visible(locator, timeout) element.click() element.send_keys(text) def screenshot(self, file_name="screenshot.png"): self.driver.save_screenshot(file_name) def text_exists(self, text, timeout=8): try: locator = ( AppiumBy.ANDROID_UIAUTOMATOR, f'new UiSelector().textContains("{text}")', ) WebDriverWait(self.driver, timeout).until( EC.presence_of_element_located(locator) ) return True except Exception: return Falsetext_exists方法用于断言当前页面是否出现指定文本。它使用 UiSelector 的 textContains 匹配,适合“加入购物车”这类容易包含在按钮或标题中的文案。
5.5 编写首页、搜索页与商品详情页
首页页面对象类如下:
# 文件路径:pages/home_page.py from appium.webdriver.common.appiumby import AppiumBy from pages.base_page import BasePage from pages.search_page import SearchPage class HomePage(BasePage): # 网易严选不同版本首页搜索入口文案可能有差异,请用 Appium Inspector 确认 SEARCH_ENTRY = ( AppiumBy.ANDROID_UIAUTOMATOR, 'new UiSelector().textContains("搜索")', ) def open_search(self): self.click(self.SEARCH_ENTRY) return SearchPage(self.driver)如果首页顶部搜索框文案不是“搜索”,而是“搜索严选商品”或某个输入框 icon,只需要把定位改成实际控件即可。搜索页对象类如下:
# 文件路径:pages/search_page.py from appium.webdriver.common.appiumby import AppiumBy from pages.base_page import BasePage from pages.goods_page import GoodsPage class SearchPage(BasePage): SEARCH_INPUT = ( AppiumBy.ANDROID_UIAUTOMATOR, 'new UiSelector().className("android.widget.EditText")', ) def search(self, keyword): self.send_keys(self.SEARCH_INPUT, keyword) # 部分页面没有搜索按钮,按回车键触发搜索 self.driver.press_keycode(66) def enter_first_result(self, keyword): # 搜索后,通常标题里会包含搜索关键词 result_item = ( AppiumBy.ANDROID_UIAUTOMATOR, f'new UiSelector().textContains("{keyword}")', ) self.click(result_item) return GoodsPage(self.driver)enter_first_result默认点击第一个包含关键词的文本控件。如果真实页面的搜索结果被广告或推荐位干扰,这里可能需要改成点击某一种商品卡片容器,但原理相同:先等待条件满足,再找到可点击元素。
商品详情页对象类如下:
# 文件路径:pages/goods_page.py from appium.webdriver.common.appiumby import AppiumBy from pages.base_page import BasePage class GoodsPage(BasePage): ADD_CART_TEXT = "加入购物车" def has_add_cart_button(self): return self.text_exists(self.ADD_CART_TEXT)商品详情页结构复杂,这里只做“加入购物车按钮是否存在”的断言。若页面中该按钮是导航栏上的一个 icon,而文案出现在别的位置,同样可以通过 Inspector 观察后调整。
5.6 编写 pytest 测试用例
测试用例通过 pytest 管理,这里把 driver 初始化放进 fixture,保证用例结束后能正确退出会话。
# 文件路径:tests/test_yx_search.py import pytest from appium import webdriver from caps import get_caps from pages.home_page import HomePage @pytest.fixture(scope="module") def driver(): base_url = "http://127.0.0.1:4723/wd/hub" appium_driver = webdriver.Remote(base_url, get_caps()) yield appium_driver appium_driver.quit() def test_search_goods_to_detail(driver): # 启动后进入网易严选首页 home_page = HomePage(driver) # 点击搜索入口 search_page = home_page.open_search() # 输入关键词并搜索 search_page.search("保温杯") # 进入第一个搜索结果商品详情 goods_page = search_page.enter_first_result("保温杯") # 断言详情页出现“加入购物车”按钮 assert goods_page.has_add_cart_button() # 截图作为测试证据 goods_page.screenshot("goods_detail.png")测试用例本身非常接近自然语言。这样做的好处是:以后不管底层元素定位怎么变化,只要页面对象类保持不变,测试人员看用例就能理解业务意图。
5.7 运行与结果说明
先确保 Appium Server 在运行,再进入项目虚拟环境执行:
pytest tests/test_yx_search.py -s -v --html=report.html如果一切正常,终端会类似输出:
PASSED tests/test_yx_search.py::test_search_goods_to_detail同时会在项目根目录生成report.html和goods_detail.png。打开截图,可以看到脚本确实操作到了商品详情页。
假如测试失败,优先检查失败弹窗和页面截图。因为BasePage中用了显示等待,很多失败不是“没有元素”,而是等待超时。超时原因通常是定位表达式的文案与当前页面不一致,或者页面带着开屏广告/弹窗。这时打开 Appium Inspector 人工查看一下当前页面结构,很快能定位问题。
6. 常见问题与排查思路
6.1 问题速查表
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 创建 Session 失败 | Appium Server 未启动或 Capabilities 格式错误 | 检查 Appium 终端日志,确认 4723 端口可访问 |
| 找不到设备 | adb 未识别真机或模拟器 | 执行 adb devices 确认设备状态为 device |
| 点击搜索入口后无反应 | 首页文本和定位表达式不匹配 | 用 Appium Inspector 重新获取实际文本或 resource-id |
| NoSuchElementException | 元素尚未加载完成或定位无效 | 使用显式等待,替换更合适的选择器 |
| send_keys 输入中文乱码 | 输入法不是 Appium Unicode 键盘 | Capabilities 中增加 unicodeKeyboard 和 resetKeyboard |
| 点击元素报 stale element | 页面刷新后旧元素对象失效 | 重新定位,不要让元素对象跨长时间复用 |
| 跑一次成功后第二次失败 | noReset 导致页面停留在上一个状态 | 用例前置或后置需要恢复初始页面 |
6.2 遇到 Session 创建失败怎么办
Session 创建失败是最早遇到的高频问题。先看 Appium Server 终端日志,常见错误是Could not find a connected Android device、Activity used to start app doesn't exist或UiAutomator2 driver is not installed。
如果是设备连接问题,回到adb devices检查。如果是 Activity 问题,回到设备上手动打开网易严选,再用dumpsys activity获取正确 Activity。如果是驱动问题,执行appium driver install uiautomator2。
6.3 元素定位超时怎么办
测试失败日志里出现TimeoutException时,不要急着改等待时间。先手动执行到失败那一步,观察页面真实状态。常见情况是:页面弹出了隐私协议弹窗、搜索后出现空结果、不同测试机分辨率导致控件文本重叠。针对弹窗,可以在启动流程中增加关闭弹窗逻辑;针对空结果,需要准备稳定测试关键词或者测试数据。盲目把 timeout 调大只会让用例变慢,并不能真正解决元素不存在问题。
7. 工程化与最佳实践
7.1 坚持 Page Object 模式
刚开始学习 UI 自动化时,很容易把页面的元素定位直接写在测试用例里,比如:
driver.find_element(...).click()这样写几个用例还能忍受,用例一旦超过 20 条,页面结构调整一次就会改到崩溃。Page Object 模式的核心,是一个页面一个类,页面元素和操作都封装在类里。测试用例和页面细节隔离后,日常维护工作量会