Appium+Python+Pytest:从零编写你的第一个App UI自动化测试脚本

📅 2026/7/5 9:32:27
Appium+Python+Pytest:从零编写你的第一个App UI自动化测试脚本
1. 项目概述从零到一搞定你的第一个App UI自动化测试脚本如果你是一名移动端测试工程师或者是一名对质量保障感兴趣的后端、前端开发那么“UI自动化测试”这个词你一定不陌生。它听起来很酷能解放双手让机器代替人工去点点点但很多朋友在尝试迈出第一步时就被Appium、环境配置、元素定位这些“拦路虎”给劝退了。今天我们不谈那些庞大复杂的自动化测试框架就聚焦一件事如何从零开始亲手写出并成功运行你的第一个App UI自动化测试脚本。这个脚本的目标很简单能自动打开一个App比如手机自带的计算器完成一个简单的操作比如计算11并验证结果是否正确。通过这个最小化的实践你将打通从环境搭建、脚本编写到执行调试的完整链路理解UI自动化测试的核心工作流。无论你是测试新手想入门还是开发同学想为自己的App增加自动化回归能力这篇内容都将提供一条清晰、可落地的路径。我会基于最主流的Appium Python Pytest技术栈来讲解这是目前社区最活跃、资源最丰富的组合能让你在遇到问题时快速找到解决方案。2. 环境准备搭建你的自动化“工作台”写脚本之前得先把“厨房”收拾好。环境配置是新手最容易卡住的地方但只要我们按步骤来完全可以避开大部分坑。我们的目标是搭建一个能够驱动手机或模拟器并执行Python脚本的环境。2.1 核心工具安装与配置首先我们需要三样核心工具Java、Android SDK、Appium Server。它们的关系可以这样理解Appium Server是一个翻译官它接收我们用Python写的指令如“点击登录按钮”并将其翻译成手机系统Android/iOS能理解的底层命令。而Android SDK提供了与Android设备通信的工具Java则是Appium Server的运行环境。第一步安装Java JDKAppium Server是基于Node.js的但其底层依赖Java环境。建议安装JDK 8或JDK 11LTS版本。访问Oracle官网或AdoptOpenJDK等开源站点下载对应系统的JDK安装包。安装后需要配置环境变量。以Windows为例新建系统变量JAVA_HOME值为你的JDK安装路径如C:\Program Files\Java\jdk1.8.0_301。在系统变量Path中添加%JAVA_HOME%\bin。打开命令行输入java -version和javac -version能显示版本信息即说明配置成功。第二步安装Android SDKAndroid SDK我们通常不需要完整安装只需要其命令行工具包Command-line Tools和必要的平台工具。下载Android Command Line Tools。你可以通过Android Studio安装或直接下载独立的命令行工具包。解压到一个目录例如D:\Android\cmdline-tools。将其下的bin目录路径如D:\Android\cmdline-tools\bin添加到系统环境变量Path中。我们需要两个关键工具adb(Android Debug Bridge) 和appium需要的uiautomatorviewer。它们通常在platform-tools和tools目录下。通过SDK管理器安装打开命令行运行sdkmanager “platform-tools” “platforms;android-30” “emulator”版本号可调整。这会安装平台工具、指定API级别的系统镜像和模拟器。安装后将platform-tools的路径如D:\Android\platform-tools也添加到Path环境变量。验证命令行输入adb version应能显示版本号。第三步安装Appium ServerAppium Server有两种形式桌面版Appium Desktop和命令行版Appium Server。对于初学者强烈推荐使用Appium Desktop它自带了一个元素定位器Inspector图形化界面更友好。从Appium官网的Release页面下载对应系统的Appium Desktop安装包。安装并启动。你会看到一个简单的界面输入Host默认127.0.0.1和Port默认4723点击“Start Server”按钮。看到日志输出启动成功即可。保持这个窗口运行我们的Python脚本将连接这个端口。注意环境变量配置后务必关闭所有旧的命令行窗口并重新打开新的以使配置生效。这是很多“命令找不到”问题的根源。2.2 Python环境与依赖库安装我们的脚本将用Python来写。Python环境的管理我推荐使用Miniconda或Virtualenv创建独立的虚拟环境避免包冲突。安装Python从Python官网下载3.7及以上版本建议3.8或3.9的安装包。安装时务必勾选“Add Python to PATH”。创建虚拟环境以conda为例# 创建一个名为appium_test的虚拟环境指定Python版本 conda create -n appium_test python3.8 # 激活环境 conda activate appium_test安装核心Python库在激活的虚拟环境中使用pip安装以下库。pip install Appium-Python-Client pip install pytest pip install seleniumAppium-Python-Client这是Appium官方提供的Python客户端库封装了所有与Appium Server通信的协议。pytest一个强大的测试框架我们将用它来组织和运行我们的测试用例它比Python自带的unittest更简洁灵活。selenium虽然我们测的是App但Appium遵循了WebDriver协议由Selenium制定很多底层交互依赖这个库。至此你的自动化“工作台”就基本搭建好了。接下来我们需要一个被测设备。2.3 被测设备准备与连接你可以使用真机或模拟器。对于第一个脚本我建议使用Android模拟器因为它更干净、稳定且可以随意重置。使用Android Studio创建模拟器打开Android Studio进入“AVD Manager”Android Virtual Device Manager。点击“Create Virtual Device”选择一个中等配置的设备如Pixel 4下载一个系统镜像建议选择API Level 30左右的版本完成创建。启动模拟器在AVD Manager中点击启动。等待模拟器完全启动进入主界面。连接设备在命令行中输入adb devices。你应该能看到一个设备列表其中包含你的模拟器例如emulator-5554 device。这表示设备已连接可以被ADB和Appium识别。获取被测App信息我们需要知道被测App的“包名”和“启动Activity”。对于系统自带计算器在模拟器中打开计算器App。命令行输入adb shell dumpsys window | findstr mCurrentFocus(Windows) 或adb shell dumpsys window | grep mCurrentFocus(Mac/Linux)。输出类似mCurrentFocusWindow{... com.android.calculator2/com.android.calculator2.Calculator}。这里com.android.calculator2就是包名appPackagecom.android.calculator2.Calculator就是启动ActivityappActivity。如果你使用真机需要先开启手机的“开发者选项”和“USB调试”模式然后用USB线连接电脑同样通过adb devices验证连接。3. 脚本核心编写你的第一个测试用例环境就绪设备连上现在可以开始写代码了。我们将创建一个Python文件例如test_first_calculator.py。3.1 初始化驱动建立与设备的会话所有UI自动化的操作都通过一个叫driver的对象来进行。初始化这个驱动就是告诉Appium“我要测试哪个设备上的哪个App”。import pytest from appium import webdriver from appium.webdriver.common.appiumby import AppiumBy import time class TestCalculator: classmethod def setup_class(cls): 整个测试类开始前执行一次用于初始化驱动 # 定义Desired Capabilities这是一组键值对用于告知Appium Server测试的上下文 desired_caps { platformName: Android, # 测试平台 platformVersion: 11, # 平台版本根据你的模拟器/真机系统填写 deviceName: emulator-5554, # 设备名通过adb devices获取 automationName: UiAutomator2, # Android自动化引擎推荐使用UiAutomator2 appPackage: com.android.calculator2, # 被测App包名 appActivity: com.android.calculator2.Calculator, # 被测App启动Activity noReset: True, # 不重置App数据避免每次清空缓存 newCommandTimeout: 600, # 命令超时时间秒 unicodeKeyboard: True, # 启用Unicode输入法用于输入中文等 resetKeyboard: True # 测试结束后重置回默认输入法 } # 初始化驱动连接本地Appium Server cls.driver webdriver.Remote(http://localhost:4723/wd/hub, desired_caps) # 隐式等待设置一个全局的元素查找超时时间 cls.driver.implicitly_wait(10) classmethod def teardown_class(cls): 整个测试类结束后执行一次用于清理关闭驱动 if cls.driver: cls.driver.quit()关键点解析Desired Capabilities这是Appium的核心配置。它本质上是一个JSON对象告诉Appium Server你想要如何启动会话。platformName,deviceName,appPackage,appActivity是必须项。webdriver.Remote这里我们连接的是本地(localhost)的Appium Server端口4723。如果你的Appium Server运行在其他机器上需要修改这里的地址。implicitly_wait隐式等待。它不是一个固定的休眠(time.sleep)而是设置一个最大等待时间。在查找元素时如果元素没有立即出现WebDriver会轮询查找直到找到或超时。这比硬编码的time.sleep更智能、高效。3.2 元素定位与操作让脚本“看见”并“点击”UI自动化的本质是定位元素和执行操作。定位元素就像是给脚本一双眼睛告诉它要操作屏幕上的哪个按钮、哪个输入框。Appium支持多种定位方式最常用的是通过资源IDresource-id、文本text和XPath。我们可以使用Appium Desktop内置的Inspector来帮助我们定位元素。启动Inspector在Appium Desktop主界面点击“Start Inspector Session”按钮。配置Capabilities在弹出的窗口中填入和上面脚本中类似的Desired Capabilities注意也要填对Appium Server地址和端口。连接并查看点击“Start Session”Appium会启动被测App并打开一个可以查看UI层级的窗口。你可以点击屏幕上的元素右侧会显示该元素的各种属性如resource-id,text,content-desc,class等。假设我们通过Inspector发现计算器上的数字“1”按钮其resource-id是com.android.calculator2:id/digit_1加号“”按钮的resource-id是com.android.calculator2:id/op_add等号“”按钮的resource-id是com.android.calculator2:id/eq结果文本框的resource-id是com.android.calculator2:id/result。现在我们来编写一个测试方法实现11的计算def test_addition(self): 测试计算器加法功能1 1 2 driver self.driver # 1. 定位并点击数字1 digit_1 driver.find_element(AppiumBy.ID, com.android.calculator2:id/digit_1) digit_1.click() time.sleep(0.5) # 短暂等待便于观察实际项目中可优化 # 2. 定位并点击加号 op_add driver.find_element(AppiumBy.ID, com.android.calculator2:id/op_add) op_add.click() time.sleep(0.5) # 3. 再次点击数字1 digit_1.click() time.sleep(0.5) # 4. 定位并点击等号 eq driver.find_element(AppiumBy.ID, com.android.calculator2:id/eq) eq.click() time.sleep(1) # 等待计算完成 # 5. 定位结果框获取文本并断言 result driver.find_element(AppiumBy.ID, com.android.calculator2:id/result) actual_result result.text print(f计算结果为{actual_result}) # 使用pytest的assert进行断言 assert actual_result 2, f断言失败期望结果是2实际结果是{actual_result}操作与定位详解driver.find_element(AppiumBy.ID, ‘id’)这是通过元素的资源ID进行定位是最快、最稳定的定位方式。AppiumBy.ID对应Android中的resource-id属性。.click()对定位到的元素执行点击操作。result.text获取元素的文本内容。对于计算器的结果框这里应该显示计算结果的数字。assert这是测试的核心。我们预期11等于2所以断言结果文本等于‘2’。如果不等测试将失败并打印出自定义的错误信息。3.3 运行与调试见证自动化时刻脚本写好了让我们来运行它。确保你的Appium Desktop Server正在运行端口4723。确保你的Android模拟器或真机已启动并连接adb devices可见。在命令行中切换到你的脚本所在目录并激活之前创建的Python虚拟环境。使用pytest运行测试pytest test_first_calculator.py -v-v参数会让输出更详细。如果一切顺利你将看到模拟器上的计算器被自动打开数字1、加号、数字1、等号被依次点击最后pytest输出绿色的PASSED字样。恭喜你你的第一个UI自动化测试脚本成功运行了如果失败了别担心。查看命令行输出的错误信息。常见问题有无法连接到Appium Server检查Appium Desktop是否启动端口是否为4723。找不到设备运行adb devices确认设备在线且状态为device。找不到元素最常见。可能是定位符写错了或者页面还没加载出来。检查Inspector中的属性值是否完全匹配注意大小写和空格。可以尝试增加隐式等待时间或在关键操作前添加显式等待。4. 从脚本到用例优化与最佳实践第一个脚本能跑通只是一个开始。要写出健壮、可维护的自动化测试我们需要引入一些工程化的思想和最佳实践。4.1 使用Page Object模式组织代码直接把所有定位和操作写在测试方法里当页面元素变更时你需要修改所有相关的测试方法维护成本极高。Page Object (PO) 模式是UI自动化的标准设计模式。其核心思想是将页面对象和测试逻辑分离。页面对象类封装一个页面的所有元素定位和基本操作。测试用例类调用页面对象提供的方法来完成业务逻辑并包含断言。我们为计算器创建一个页面对象类calculator_page.pyfrom appium.webdriver.common.appiumby import AppiumBy from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC class CalculatorPage: def __init__(self, driver): self.driver driver # 定义页面元素定位器统一管理 self.digit_1_locator (AppiumBy.ID, com.android.calculator2:id/digit_1) self.digit_2_locator (AppiumBy.ID, com.android.calculator2:id/digit_2) self.op_add_locator (AppiumBy.ID, com.android.calculator2:id/op_add) self.op_sub_locator (AppiumBy.ID, com.android.calculator2:id/op_sub) self.eq_locator (AppiumBy.ID, com.android.calculator2:id/eq) self.result_locator (AppiumBy.ID, com.android.calculator2:id/result) self.clear_locator (AppiumBy.ID, com.android.calculator2:id/clr) # 封装页面操作 def click_digit_1(self): 点击数字1 self.driver.find_element(*self.digit_1_locator).click() return self # 支持链式调用 def click_digit_2(self): self.driver.find_element(*self.digit_2_locator).click() return self def click_add(self): self.driver.find_element(*self.op_add_locator).click() return self def click_equals(self): self.driver.find_element(*self.eq_locator).click() return self def clear(self): self.driver.find_element(*self.clear_locator).click() return self def get_result(self): 获取计算结果使用显式等待确保结果已更新 # 显式等待等待结果元素出现并且其文本不为空 result_element WebDriverWait(self.driver, 10).until( EC.presence_of_element_located(self.result_locator) ) # 再等待一下确保文本稳定例如从‘’变成‘2’ WebDriverWait(self.driver, 5).until( lambda driver: result_element.text ! ) return result_element.text然后我们的测试脚本就变得非常简洁和易读import pytest from appium import webdriver from calculator_page import CalculatorPage # 导入页面对象 class TestCalculatorWithPO: ... # setup_class和teardown_class与之前相同 def test_addition_with_po(self): 使用Page Object模式测试加法 calc_page CalculatorPage(self.driver) # 链式调用清晰表达业务流1 1 calc_page.click_digit_1().click_add().click_digit_1().click_equals() actual_result calc_page.get_result() assert actual_result 2 def test_subtraction_with_po(self): 测试减法2 - 1 1 calc_page CalculatorPage(self.driver) calc_page.clear() # 清空之前的结果 calc_page.click_digit_2().click_subtract().click_digit_1().click_equals() assert calc_page.get_result() 1优势可维护性元素定位符只存在于页面对象类中。如果计算器UI改版按钮ID变了你只需要修改calculator_page.py中的一个地方。可读性测试用例读起来就像自然语言清晰地描述了“做什么”而不是“怎么做”。复用性click_digit_1()这样的方法可以在多个测试用例中被复用。4.2 引入显式等待与更智能的等待策略之前我们用了implicitly_wait隐式等待和time.sleep强制等待。隐式等待是全局的不够灵活time.sleep是固定等待效率低下且不可靠网络或设备慢时可能不够用。显式等待Explicit Wait是更优的选择。它允许你为某个特定的条件设置等待条件成立则立即继续超时则抛出异常。我们在上面的get_result方法中已经使用了。更常见的用法是封装一个查找元素的工具方法它结合了显式等待from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC def find_element_with_wait(driver, locator, timeout10): 带显式等待的元素查找方法 :param driver: WebDriver实例 :param locator: 定位元组如 (AppiumBy.ID, id) :param timeout: 超时时间默认10秒 :return: 找到的WebElement try: element WebDriverWait(driver, timeout).until( EC.presence_of_element_located(locator) ) return element except TimeoutException: # 可以在这里记录日志或截图方便排查 driver.save_screenshot(felement_not_found_{locator[1]}.png) raise TimeoutException(f在{timeout}秒内未找到元素: {locator})在页面对象中可以这样使用def click_digit_1(self): element find_element_with_wait(self.driver, self.digit_1_locator) element.click() return self常用等待条件ECpresence_of_element_located: 元素出现在DOM中不一定可见。visibility_of_element_located: 元素可见长宽都大于0。element_to_be_clickable: 元素可见且可点击。text_to_be_present_in_element: 元素的文本包含特定文字。4.3 测试数据驱动与参数化一个测试方法往往需要测试多组数据。例如测试加法我们想测112235等等。使用pytest的pytest.mark.parametrize装饰器可以轻松实现数据驱动。import pytest class TestCalculatorDataDriven: ... # 初始化部分省略 pytest.mark.parametrize(num1, num2, expected, [ (1, 1, 2), (2, 3, 5), (5, -2, 3), # 测试负数 (0, 9, 9), # 测试0 ]) def test_addition_with_data(self, num1, num2, expected): 数据驱动测试加法 calc_page CalculatorPage(self.driver) calc_page.clear() # 这里需要一个能点击任意数字的方法我们可以扩展页面对象 # 假设我们新增了一个方法 click_digit(number) calc_page.click_digit(num1).click_add().click_digit(num2).click_equals() actual_result int(calc_page.get_result()) # 结果转为整数比较 assert actual_result expected, f{num1} {num2} 应该等于 {expected}, 但得到 {actual_result}这需要你扩展CalculatorPage类增加一个根据数字点击对应按钮的方法。这可以通过一个数字到定位符的映射字典来实现。数据驱动将测试逻辑与测试数据分离使得增加新的测试用例只需要在参数列表中添加一行数据极大地提高了测试的覆盖率和编写效率。5. 常见问题排查与实战技巧在实际编写和运行脚本时你一定会遇到各种各样的问题。这里我总结了一些高频问题和处理技巧。5.1 元素定位失败问题深度解析这是UI自动化中最常见的问题没有之一。问题现象NoSuchElementException或TimeoutException。排查步骤确认上下文你确定当前在正确的页面/WebView/Native View吗混合应用或H5页面中需要在不同的上下文Context间切换。使用driver.contexts和driver.current_context来检查和切换。使用Appium Inspector复核在脚本运行失败时立刻用Appium Inspector连接当前会话使用相同的Capabilities查看此时的UI树结构确认你使用的定位符是否还存在、是否唯一。动态ID或列表项是重灾区。尝试其他定位方式如果ID是动态的每次打开都变化尝试用XPath结合其他稳定属性如text、content-desc或class。例如//android.widget.Button[text‘确定’]。等待策略是否因为页面加载慢导致元素还没出现将隐式等待调大或使用前面提到的显式等待。屏幕尺寸与坐标极少数情况下对于无法通过属性定位的元素比如游戏界面可以考虑使用坐标点击。通过driver.get_window_size()获取屏幕尺寸然后计算相对坐标。这是最后的手段因为它在不同分辨率设备上不兼容。实战技巧编写一个“安全查找”的装饰器或工具函数在元素查找失败时自动截图并记录日志这对于在CI/CD流水线中调试无头运行的测试非常有帮助。5.2 等待与同步的进阶策略除了显式等待还有一些复杂场景需要处理等待页面跳转/活动切换在点击一个按钮后可能会启动新的Activity或加载新页面。可以等待旧Activity消失或新Activity出现。# 假设点击登录按钮后会跳转到MainActivity current_activity driver.current_activity login_button.click() WebDriverWait(driver, 10).until( lambda x: x.current_activity ! current_activity ) # 或者等待特定的Activity出现 WebDriverWait(driver, 10).until( EC.visibility_of_element_located((AppiumBy.ID, ‘main_page_element_id’)) )等待网络请求完成对于加载列表或提交表单的场景可以等待某个代表加载完成的元素如“加载中”提示消失或者等待列表项数量不再增加。使用轮询代替固定等待彻底避免使用time.sleep。对于需要等待某个状态变化的场景使用WebDriverWait配合自定义条件函数。5.3 脚本稳定性提升与异常处理不稳定的脚本比没有脚本更糟糕因为它会带来误报消耗信任。异常处理与重试机制对于网络波动、临时弹窗如权限申请、升级提示等导致的偶发性失败可以引入重试逻辑。pytest有插件如pytest-rerunfailures可以直接给用例添加重试注解pytest.mark.flaky(reruns2)。用例独立性每个测试用例都应该是独立的不依赖其他用例的执行状态。这意味着需要在setup_method每个用例开始前或teardown_method每个用例结束后进行清理比如重置App、清理数据、回到主页面等。可以使用driver.reset()或driver.start_activity(package, activity)来重启App到初始状态。截图与日志在关键步骤如用例开始、结束、断言前和每次失败时自动截图并配合详细的日志输出可以使用Python的logging模块这是事后排查问题的黄金资料。def take_screenshot(driver, name): timestamp time.strftime(“%Y%m%d_%H%M%S”) filename f”screenshot_failure_{name}_{timestamp}.png” driver.save_screenshot(filename) print(f”截图已保存: {filename}”) # 也可以将文件名记录到测试报告中处理系统弹窗与权限在测试开始前通过ADB命令预先授予App所需权限可以避免测试过程中被权限弹窗打断。adb shell pm grant package_name android.permission.PERMISSION_NAME5.4 在CI/CD中集成与执行个人跑通只是第一步最终目标是集成到团队的持续集成流水线中每次代码提交都能自动运行。环境一致性问题CI服务器如Jenkins、GitLab CI上的环境必须与本地开发环境一致。使用Docker镜像来封装测试环境包含JDK、Android SDK、Appium、Python环境、依赖库是最佳实践。可以寻找或自己构建包含这些工具的Docker镜像。设备管理在CI上可以使用Android模拟器但需要以无头模式运行。也可以使用云测平台如国内的Testin、腾讯WeTest国外的BrowserStack、Sauce Labs提供的真机设备池它们通常提供了与Appium兼容的接口。测试报告使用pytest-html或allure-pytest生成漂亮的HTML测试报告。Allure报告尤其强大可以展示用例层级、步骤、截图、日志非常适合团队协作和问题分析。执行策略在CI中通常只运行冒烟测试或核心回归用例。可以将测试用例用pytest的mark功能分类如pytest.mark.smoke然后在CI脚本中指定只运行这些标记的用例pytest -m smoke。第一个脚本的完成是你踏入App UI自动化测试大门的第一步。这条路的关键不在于记住所有API而在于理解“驱动-定位-操作-断言”这个核心循环并掌握Page Object、数据驱动、等待策略这些设计模式来构建健壮的测试套件。当你遇到问题时多查官方文档多利用Inspector工具多思考元素背后的UI层级和状态变化你会发现很多问题都能迎刃而解。自动化测试的最终目的不是取代手工测试而是将测试人员从重复、枯燥的回归工作中解放出来去从事更有价值的探索性测试和测试设计工作。