从零搭建pytest+Appium+Allure移动端UI自动化测试框架实战

📅 发布时间:2026/7/4 14:48:15 👁️ 浏览次数:
从零搭建pytest+Appium+Allure移动端UI自动化测试框架实战
1. 项目概述构建一个现代化的移动端UI自动化测试框架如果你正在为移动端应用的回归测试、兼容性测试或者持续集成中的UI自动化环节而头疼那么今天分享的这个“pytestappiumallure”组合拳项目实例或许就是你一直在找的解决方案。我花了将近一周时间从零开始搭建并完善了这个框架它不仅仅是一个简单的脚本集合而是一个结构清晰、易于维护、报告美观的完整测试工程。核心目标很明确用Python的pytest测试框架驱动Appium进行移动端Android/iOS的UI自动化操作最后通过Allure生成可视化、可交互的测试报告。这套组合在业内已经非常成熟但网上很多资料要么过于零散要么只讲理论真正能跑起来、能应对实际项目中各种坑的完整实例并不多。这篇文章我就把我从环境搭建、框架设计、用例编写到报告生成的全过程以及中间踩过的那些“坑”毫无保留地分享出来希望能帮你快速上手少走弯路。2. 环境准备与核心工具链解析搭建任何自动化测试框架第一步永远是搞定环境。这一步看似基础却拦住了至少一半的初学者。很多人卡在Appium服务启动不了、模拟器连接不上或者Allure报告出不来。下面我按照实际操作的顺序带你一步步走通。2.1 Python与Pytest环境搭建首先Python是这一切的基础。我强烈建议使用Python 3.7及以上版本因为很多新的库对低版本支持不佳。不要用系统自带的Python用pyenv或者直接去官网下载安装包管理起来会更干净。安装好Python后第一件事就是创建虚拟环境。这是Python项目开发的黄金法则能有效隔离不同项目的依赖避免版本冲突。在项目根目录下执行python3 -m venv venv # Windows用户 venv\Scripts\activate # Mac/Linux用户 source venv/bin/activate激活虚拟环境后命令行提示符前会出现(venv)字样。接下来安装核心的测试框架pytest。pytest的强大之处在于其简洁的语法、丰富的插件生态和强大的断言机制。我们还需要安装一些配套插件。pip install pytest pytest-html pytest-xdist allure-pytest这里解释一下这几个包pytest: 测试框架本体。pytest-html: 生成基础的HTML测试报告作为Allure的备选或快速预览。pytest-xdist: 实现测试用例的分布式执行可以并行跑用例大幅提升执行效率尤其是在多设备测试时。allure-pytest: 这是连接pytest和Allure报告的关键桥梁它会在测试执行时收集必要的数据。注意安装allure-pytest时可能会遇到依赖冲突。如果报错可以尝试先升级pip和setuptoolspip install --upgrade pip setuptools。2.2 Appium服务端与客户端的部署Appium是一个开源工具用于自动化移动端原生、混合和移动Web应用。它采用C/S架构我们需要分别部署服务端和客户端。服务端安装Appium服务端推荐通过Node.js的npm安装这是最通用和方便的方式。# 1. 安装Node.js (如果未安装) # 去Node.js官网下载安装包或者用brew (Mac) / apt-get (Linux)安装。 # 2. 通过npm安装Appium npm install -g appium # 3. 安装Appium Doctor检查环境 npm install -g appium-doctor安装完成后运行appium-doctor它会检查你的Android和iOS开发环境是否完备如JAVA_HOME, ANDROID_HOME, 是否有可用的模拟器或真机。根据它的提示缺什么补什么。这是排查环境问题最有效的工具。客户端库安装我们的Python脚本是Appium的客户端需要通过Appium-Python-Client库与Appium服务端通信。pip install Appium-Python-Client这个库封装了WebDriver协议让我们可以用类似Selenium的语法来操作手机App。2.3 Allure报告工具的安装与配置Allure报告以其强大的交互性和美观的界面著称但它的安装稍微麻烦一点因为它本身是一个Java工具。对于Mac用户使用Homebrew:brew install allure对于Windows/Linux用户: 需要去Allure的GitHub Releases页面下载zip包解压后将其bin目录添加到系统的PATH环境变量中。例如在Windows上你需要将D:\allure\bin这样的路径加到系统环境变量的Path里。安装完成后在命令行输入allure --version如果显示版本号则说明安装成功。这里有个巨坑有时即使PATH配置正确命令仍提示“不是内部或外部命令”。这通常是因为终端没有重启或刷新环境变量。关闭当前命令行窗口重新打开一个新的再试或者直接重启电脑。最后我们需要一个命令行工具来生成和打开报告这通过allure-pytest插件收集数据再用allure命令生成。3. 项目结构与PO设计模式实战一个可维护的自动化测试项目必须有清晰合理的目录结构。直接上代码堆在一起后期改起来会是灾难。我采用的是一种基于“页面对象模型”的改进结构融合了pytest的特性。3.1 核心目录结构设计我的项目目录树是这样的pytest_appium_allure_demo/ ├── apk/ # 存放待测应用的安装包 │ └── demo.apk ├── config/ # 配置文件 │ ├── __init__.py │ └── config.yaml # 测试配置如设备信息、服务器地址 ├── logs/ # 运行日志 ├── reports/ # 测试报告Allure原始数据、HTML报告 │ ├── allure-results/ │ └── allure-report/ ├── page_objects/ # 页面对象层 │ ├── __init__.py │ ├── base_page.py # 基类封装公共方法 │ ├── login_page.py # 登录页面 │ └── main_page.py # 主页面 ├── test_cases/ # 测试用例层 │ ├── __init__.py │ ├── conftest.py # pytest共享fixture │ ├── test_login.py │ └── test_home.py ├── utils/ # 工具层 │ ├── __init__.py │ ├── driver_manager.py # 驱动管理单例/多设备 │ └── logger.py # 日志工具 ├── .gitignore ├── pytest.ini # pytest配置文件 ├── requirements.txt # 项目依赖 └── run_tests.py # 测试运行入口脚本这个结构的关键在于分层config/: 隔离配置不同环境测试/预发/生产只需改配置文件。page_objects/: 实现PO模式将页面的元素定位和操作封装成类测试用例只调用业务方法不直接操作元素。这是提升代码可维护性的核心。test_cases/: 存放纯粹的测试逻辑用pytest的test_*.py文件组织。utils/: 封装通用功能如驱动管理、日志、文件读取避免代码重复。3.2 深入理解PO模式与BasePage封装页面对象模型的核心思想是“将页面封装成对象将操作封装成方法”。我们首先创建一个BasePage类它继承自Appium的WebDriver并封装所有页面都可能用到的方法比如查找元素、点击、输入、滑动等。这样做的好处是一旦Appium的API有变动或者我们想增加一些通用操作比如带重试机制的点击只需要修改这一个基类。# page_objects/base_page.py from appium.webdriver.webdriver import WebDriver from appium.webdriver.common.appiumby import AppiumBy from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC import logging class BasePage: def __init__(self, driver: WebDriver): self.driver driver self.logger logging.getLogger(__name__) self.wait WebDriverWait(self.driver, 10) # 显式等待10秒 def find_element(self, locator, timeout10): 查找单个元素支持多种定位方式 by, value locator try: element WebDriverWait(self.driver, timeout).until( EC.presence_of_element_located((by, value)) ) self.logger.info(f找到元素: {locator}) return element except Exception as e: self.logger.error(f查找元素失败: {locator}, 错误: {e}) # 这里可以添加截图操作方便排查 self.driver.save_screenshot(ferror_find_{value}.png) raise e def click(self, locator): 点击元素封装了常见的点击失败重试逻辑 element self.find_element(locator) try: element.click() self.logger.info(f点击元素: {locator}) except Exception as e: # 有时元素可点击但点击无效尝试用JavaScript点击 self.logger.warning(f常规点击失败尝试JS点击: {locator}) self.driver.execute_script(arguments[0].click();, element) def input_text(self, locator, text): 输入文本先清空再输入 element self.find_element(locator) element.clear() element.send_keys(text) self.logger.info(f在元素 {locator} 中输入文本: {text}) # 更多封装方法swipe, get_text, is_displayed 等...然后具体的页面类如LoginPage继承BasePage并定义该页面特有的元素和方法。# page_objects/login_page.py from .base_page import BasePage from appium.webdriver.common.appiumby import AppiumBy class LoginPage(BasePage): # 元素定位器统一管理 USERNAME_INPUT (AppiumBy.ID, com.demo.app:id/username) PASSWORD_INPUT (AppiumBy.ID, com.demo.app:id/password) LOGIN_BUTTON (AppiumBy.ID, com.demo.app:id/login) ERROR_MSG (AppiumBy.ID, com.demo.app:id/error) def login(self, username, password): 登录业务方法 self.input_text(self.USERNAME_INPUT, username) self.input_text(self.PASSWORD_INPUT, password) self.click(self.LOGIN_BUTTON) def get_error_message(self): 获取错误提示信息 return self.find_element(self.ERROR_MSG).text在测试用例中我们只需要这样写非常清晰# test_cases/test_login.py def test_login_success(login_page): login_page.login(valid_user, valid_pass) # 断言登录成功例如跳转到主页 assert login_page.driver.current_activity .MainActivity这种写法将定位信息容易变和业务操作相对稳定从测试用例中剥离当UI元素ID变化时你只需要修改LoginPage类中的定位器所有相关的测试用例都不需要改动。4. 驱动管理与Pytest Fixture的深度应用自动化测试中如何管理WebDriver实例的生命周期是一个关键问题。我们希望在测试开始时创建驱动测试结束后安全退出并且最好能支持多线程并行。Pytest的fixture机制完美解决了这个问题。4.1 实现一个健壮的Driver Manager我通常在utils/driver_manager.py中创建一个驱动管理类负责驱动的创建和销毁并实现简单的单例模式对于并行测试需要更复杂的池化管理。# utils/driver_manager.py from appium import webdriver from appium.options.android import UiAutomator2Options import yaml import os class DriverManager: _driver None classmethod def get_driver(cls): if cls._driver is None: cls._driver cls._create_driver() return cls._driver classmethod def quit_driver(cls): if cls._driver: cls._driver.quit() cls._driver None staticmethod def _create_driver(): # 从配置文件读取设备能力和Appium服务器地址 config_path os.path.join(os.path.dirname(__file__), ../config/config.yaml) with open(config_path, r) as f: config yaml.safe_load(f) capabilities config[capabilities] server_url config[appium_server] # 使用UiAutomator2Options (Appium 2.x推荐) options UiAutomator2Options() options.platform_name capabilities.get(platformName) options.device_name capabilities.get(deviceName) options.app os.path.abspath(capabilities.get(app)) # 处理app路径 options.automation_name capabilities.get(automationName, UiAutomator2) options.no_reset capabilities.get(noReset, True) # 不重置应用状态 driver webdriver.Remote(server_url, optionsoptions) driver.implicitly_wait(10) # 设置隐式等待 return driver对应的config.yaml配置文件# config/config.yaml appium_server: http://127.0.0.1:4723 capabilities: platformName: Android platformVersion: 11 deviceName: Pixel_4_API_30 # 模拟器名称或真机UDID app: ./apk/demo.apk automationName: UiAutomator2 noReset: true fullReset: false4.2 使用Pytest Fixture组织测试生命周期fixture是pytest的精髓它提供了非常灵活的方式来设置前置条件、共享数据和清理资源。我们在test_cases/conftest.py中定义全局fixture。# test_cases/conftest.py import pytest from utils.driver_manager import DriverManager from page_objects.login_page import LoginPage from page_objects.main_page import MainPage import allure pytest.fixture(scopesession) def app_driver(): 会话级别的fixture整个测试会话只启动一次驱动适合非并行 driver DriverManager.get_driver() yield driver # 测试会话结束后清理 DriverManager.quit_driver() print(所有测试完成驱动已退出。) pytest.fixture def login_page(app_driver): 每次测试函数都会获取一个新的LoginPage实例 return LoginPage(app_driver) pytest.fixture def main_page(app_driver): return MainPage(app_driver) pytest.hookimpl(tryfirstTrue, hookwrapperTrue) def pytest_runtest_makereport(item, call): 钩子函数用于在测试失败时自动截图并附加到Allure报告 outcome yield rep outcome.get_result() if rep.when call and rep.failed: driver item.funcargs.get(app_driver) if driver: # 将截图以附件形式添加到Allure报告 allure.attach(driver.get_screenshot_as_png(), name失败截图, attachment_typeallure.attachment_type.PNG)这里有几个关键点scopesession: 这个app_driverfixture在整个pytest执行过程中只会被创建一次并在所有测试结束后销毁。这比每个用例都重启App要快得多。但注意这要求你的测试用例不能相互污染应用状态所以上面配置了noReset: true。yield:yield之前的代码是前置设置创建驱动yield返回的是fixture的值driver对象yield之后的代码是后置清理退出驱动。钩子函数pytest_runtest_makereport是一个强大的钩子它允许我们在测试生命周期的特定时刻插入代码。这里我们用它来实现测试失败时的自动截图并挂到Allure报告上这对排查UI问题至关重要。5. 测试用例编写、参数化与Allure报告增强环境、框架都搭好了现在可以愉快地写测试用例了。pytest让写测试变得简单而强大。5.1 编写结构清晰的测试用例一个良好的测试用例应该包含清晰的测试名、必要的准备步骤、执行操作、断言验证。我们结合PO模式和fixture来写。# test_cases/test_login.py import pytest import allure allure.epic(Demo App) # Allure报告的一级分类 allure.feature(登录模块) # 二级分类 class TestLogin: allure.story(成功登录场景) # 三级分类 allure.title(使用正确的用户名和密码登录成功) # 测试用例标题 allure.severity(allure.severity_level.BLOCKER) # 用例优先级 def test_login_success(self, login_page): 测试目的验证用户使用有效凭证可以成功登录。 前置条件应用已启动处于登录页面。 with allure.step(步骤1: 输入正确的用户名和密码): login_page.input_username(standard_user) login_page.input_password(secret_sauce) with allure.step(步骤2: 点击登录按钮): login_page.click_login_button() with allure.step(步骤3: 验证登录成功跳转到主页): # 假设登录成功会跳转到MainActivity current_activity login_page.driver.current_activity assert .MainActivity in current_activity # 或者验证主页的某个特定元素出现 assert login_page.is_element_present(login_page.PRODUCT_HEADER) allure.story(失败登录场景) allure.title(使用错误的密码登录失败) allure.severity(allure.severity_level.CRITICAL) pytest.mark.parametrize(username, password, expected_error, [ (locked_out_user, wrong_pass, 用户名或密码错误), (, secret_sauce, 用户名不能为空), (standard_user, , 密码不能为空), ]) def test_login_failure(self, login_page, username, password, expected_error): 参数化测试用一组数据测试多种失败情况。 with allure.step(f使用错误凭证登录: 用户[{username}], 密码[{password}]): login_page.login(username, password) with allure.step(验证出现正确的错误提示): actual_error login_page.get_error_message() assert actual_error expected_error, f期望错误信息: {expected_error}, 实际: {actual_error}代码解读Allure装饰器allure.epic/feature/story用于在Allure报告中创建清晰的层级结构方便过滤和查看。allure.title可以自定义用例在报告中的显示标题比函数名更友好。allure.severity标记用例优先级。with allure.step这是Allure报告的灵魂功能之一。它将测试步骤在报告中可视化展示点击可以展开/收起详情。当测试失败时你能立刻知道是哪个步骤出的问题。pytest.mark.parametrize这是pytest的“大杀器”数据驱动测试。它允许你用一个测试函数运行多组不同的输入数据和预期结果。上面的例子中test_login_failure会被执行三次每次使用不同的(username, password, expected_error)三元组。这极大地减少了重复代码。5.2 解决Allure报告中的标题换行问题一个常见的问题是当使用参数化测试且参数值较长时Allure报告中的用例标题会被挤得换行影响美观。比如test_login_failure[locked_out_user-wrong_pass-用户名或密码错误]。我们可以通过自定义allure.title来优化。allure.title(登录失败 - 用户名: {username}, 原因: {expected_error}) pytest.mark.parametrize(username, password, expected_error, [...]) def test_login_failure(self, login_page, username, password, expected_error): # ... 测试逻辑 ... # 在allure.title中可以使用参数化传入的参数生成更简洁的标题这样生成的报告标题就是“登录失败 - 用户名: locked_out_user, 原因: 用户名或密码错误”更清晰且不会因为过长而换行。6. 测试执行、报告生成与持续集成集成一切就绪是时候运行测试并查看漂亮的报告了。6.1 使用pytest命令执行测试在项目根目录下你可以使用各种pytest命令来执行测试。基本运行# 运行所有测试 pytest # 运行特定模块 pytest test_cases/test_login.py # 运行标记为critical的测试 pytest -m critical # 运行包含“login”关键字的测试 pytest -k login生成Allure结果数据Allure报告需要先收集测试运行过程中的数据JSON格式然后再生成HTML报告。# 运行测试并指定Allure结果存放目录 pytest --alluredir./reports/allure-results执行后./reports/allure-results目录下会生成一堆.json文件这就是原始数据。生成并打开HTML报告# 根据结果数据生成HTML报告 allure generate ./reports/allure-results -o ./reports/allure-report --clean # 打开生成的HTML报告 allure open ./reports/allure-reportallure generate命令的--clean选项会清空之前的报告目录。allure open会自动在默认浏览器中打开报告。6.2 编写一键运行脚本每次都敲一长串命令太麻烦我习惯创建一个run_tests.py脚本。# run_tests.py import subprocess import sys import os def run_tests(): 一键运行测试并生成报告 # 定义路径 results_dir ./reports/allure-results report_dir ./reports/allure-report # 1. 运行pytest测试收集Allure数据 print(开始执行测试...) pytest_cmd [ sys.executable, -m, pytest, -v, # 详细输出 --tbshort, # 简短的traceback f--alluredir{results_dir} ] # 可以在这里添加更多参数如 -n auto 用于并行测试 # pytest_cmd.append(-n auto) result subprocess.run(pytest_cmd) if result.returncode ! 0: print(测试执行失败) # 即使失败也尝试生成报告 # 2. 生成Allure HTML报告 print(生成Allure报告...) if os.path.exists(report_dir): subprocess.run([rm, -rf, report_dir]) # 清理旧报告 allure_cmd [allure, generate, results_dir, -o, report_dir, --clean] subprocess.run(allure_cmd) # 3. 自动打开报告可选 open_report input(测试完成是否打开报告(y/n): ).lower() if open_report y: subprocess.run([allure, open, report_dir]) else: print(f报告已生成请手动打开: file://{os.path.abspath(report_dir)}/index.html) if __name__ __main__: run_tests()6.3 集成到持续集成CI流程在Jenkins、GitLab CI、GitHub Actions等CI工具中集成此框架非常方便。核心步骤就是在CI的配置文件中执行上述命令并将Allure报告作为构建产物保存和发布。以GitHub Actions为例一个简单的.github/workflows/test.yml配置可能如下name: UI Automation Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkoutv3 - name: Set up Python uses: actions/setup-pythonv4 with: python-version: 3.9 - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt npm install -g appium - name: Start Appium Server run: | appium --log-level error sleep 10 # 等待Appium启动 - name: Run tests with Allure run: | pytest --alluredir./reports/allure-results - name: Generate Allure Report run: | allure generate ./reports/allure-results -o ./reports/allure-report --clean - name: Upload Allure Report uses: actions/upload-artifactv3 with: name: allure-report path: ./reports/allure-report/这样每次代码推送或合并请求时都会自动运行UI自动化测试并生成可下载的Allure报告。7. 实战避坑指南与高级技巧在搭建和运行这套框架的过程中我遇到了不少坑。这里总结几个最常见的问题和解决方案希望能帮你节省大量排查时间。7.1 环境与依赖问题排查表问题现象可能原因解决方案appium命令未找到Node.js或npm未正确安装/配置检查node -v和npm -v确保已安装并将npm全局目录加入PATH。allure命令未找到Allure未安装或PATH未配置确认安装步骤关闭终端重试或直接使用绝对路径运行allure。adb devices列表为空设备未连接/未授权/USB调试未开1. 检查USB线。2. 手机弹出“允许USB调试”时点击确定。3. 执行adb kill-server adb start-server。Appium Server启动后无法连接端口被占用/主机名错误1. 换端口appium -p 4724。2. 确保客户端代码中server_url与启动端口一致http://127.0.0.1:4724。提示UiAutomator2相关错误未安装io.appium.uiautomator2.server等测试服务首次在真机运行时Appium会自动安装这些服务确保手机联网。也可以手动通过adb install安装对应apk。7.2 元素定位与交互的常见陷阱元素定位不到NoSuchElementException等待策略这是最常见原因。UI渲染需要时间。永远不要只用time.sleep。优先使用WebDriverWait配合expected_conditions如presence_of_element_located,element_to_be_clickable。上下文切换在混合应用或WebView中需要先切换到正确的上下文Context。使用driver.contexts和driver.switch_to.context。动态ID或XPath避免使用绝对XPath。优先使用resource-id,accessibility-id,content-desc。对于动态ID尝试用contains,starts-with等XPath函数进行部分匹配。点击无效ElementNotInteractableException元素不可见/被遮挡检查元素是否在屏幕内是否被其他元素如弹窗遮挡。可以尝试先滚动到元素位置再点击。坐标点击作为最后手段可以获取元素坐标使用TouchAction或driver.tap进行点击。但此法不推荐因为适配性差。JavaScript点击如前面BasePage.click方法所示可以尝试用driver.execute_script(arguments[0].click();, element)。输入框输入异常有些输入框需要先点击获取焦点再send_keys。中文输入问题确保在Desired Capabilities中设置了unicodeKeyboard: True和resetKeyboard: True以使用Appium的Unicode输入法。7.3 提升框架健壮性与可维护性配置外部化将所有可变配置设备UDID、App路径、服务器地址、账号密码放到config.yaml或.env文件中通过环境变量区分不同环境测试/生产。日志系统使用Python的logging模块记录详细的运行日志包括操作步骤、元素定位信息、错误堆栈。将日志输出到文件和控制台方便回溯。失败重试机制网络波动或应用偶尔卡顿可能导致用例失败。可以使用pytest的插件pytest-rerunfailures为不稳定的用例添加重试逻辑。pip install pytest-rerunfailures运行命令pytest --reruns 3 --reruns-delay 2表示失败后重试3次每次间隔2秒。并行测试利用pytest-xdist并行执行用例结合Selenium Grid或Appium的systemPort配置可以实现多设备同时测试极大缩短测试时间。pytest -n auto # 自动检测CPU核心数并行 pytest -n 2 # 指定2个worker并行注意并行时要确保每个测试会话有独立的驱动实例不能共享app_driverfixture需要将scope改为function或实现驱动池。测试数据管理将测试数据如用户账号、商品信息与测试代码分离。可以使用JSON、YAML或Excel文件存储或者连接测试数据库。在fixture中读取数据并传递给测试函数。搭建这样一个pytestappiumallure的自动化测试框架初期投入确实需要一些时间但一旦建成它将为你带来巨大的回报快速的回归测试、精准的Bug定位、美观的测试报告和可复用的测试资产。最重要的是它把测试人员从重复的手工劳动中解放出来去从事更有价值的测试设计和探索性测试。希望这个详细的实例和其中包含的经验能成为你构建自己自动化测试体系的一块坚实基石。如果在实践过程中遇到新的问题不妨回头看看BasePage的封装是否到位、fixture的生命周期设置是否合理、或者Allure的步骤划分是否清晰很多时候问题就出在这些设计细节上。