去年接了个内部工具类App的测试改造,团队只有我一个测试开发,要在一个月内把核心流程的UI自动化跑起来。当时想得很简单:Appium + Python + Jenkins,网上教程一抓一把。真正落地才发现,环境、脚本、数据、CI四个环节,每个都有坑等着你。今天把这套方案完整复盘一遍,代码片段和配置都是生产验证过的,希望能帮你少走弯路。
背景与问题:为什么UI自动化这么难落地
项目是个物流配送App,Android和iOS双端,核心流程包括登录、接单、配送、签收。之前全是手工回归,每次发版要两个测试人员忙一天。目标很明确:把核心流程自动化,接入CI,每天跑一遍。
但一开始就遇到三个头疼的问题:环境搭建版本不兼容、脚本不稳定(今天过明天挂)、测试数据不独立。这三个坑不解决,自动化就是玩具。
环境搭建:版本匹配是第一道坎
Appium的环境复杂度堪比俄罗斯套娃——Java、Android SDK、Node、Appium Desktop、模拟器/真机驱动,每个组件都有自己偏爱的版本。我第一次按网上教程装,Appium 2.0配Java 17,结果安卓驱动死活起不来,报错信息晦涩得像天书。
折腾两天后,我换了个思路:锁定版本。用Appium 1.22.3(稳定大版本)+ Java 8 + Node 14,配合Appium Desktop 1.22.x的Inspector来定位元素。模拟器用的是Android 10的镜像,因为内部App最低支持Android 8,Android 10兼容性最好。
关键配置写在desired_caps里,我抽了个公共模块:
def get_caps(platform='android'): if platform == 'android': return { 'platformName': 'Android', 'deviceName': 'emulator-5554', 'platformVersion': '10.0', 'appPackage': 'com.express.delivery', 'appActivity': '.MainActivity', 'noReset': True, # 保留登录态,避免每次重新输入 'unicodeKeyboard': True, # 支持中文输入 'resetKeyboard': True } else: return { 'platformName': 'iOS', 'deviceName': 'iPhone 13', 'platformVersion': '15.0', 'automationName': 'XCUITest', 'app': '/path/to/app.ipa', 'noReset': True }核心洞见:版本锁定是环境搭建的第一原则。不要追求最新,要追求稳定。团队内部统一版本,写进README,新成员照着配,半小时搞定。
脚本编写:Page Object让代码可维护
刚开始写脚本,所有元素定位直接散在用例里,一个用例几百行,改个按钮ID得全局搜索替换。后来引入Page Object模式,把每个页面封装成类,元素定位和操作逻辑集中管理。
比如登录页:
class LoginPage: def __init__(self, driver): self.driver = driver self.username = (By.ID, 'com.express.delivery:id/et_username') self.password = (By.ID, 'com.express.delivery:id/et_password') self.login_btn = (By.ID, 'com.express.delivery:id/btn_login') def login(self, username, password): self.driver.find_element(*self.username).send_keys(username) self.driver.find_element(*self.password).send_keys(password) self.driver.find_element(*self.login_btn).click()元素定位优先用ID,其次用XPath。但XPath别写绝对路径,容易受布局变化影响。用相对定位,比如//android.widget.TextView[@text='接单']。
稳定性问题我用了显式等待替代sleep,封装了一个wait_element方法:
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC def wait_element(driver, locator, timeout=10): return WebDriverWait(driver, timeout).until( EC.presence_of_element_located(locator) )这个简单封装让用例稳定性从60%提升到90%以上。之前经常因为网络延迟或动画未结束导致找不到元素,现在几乎不出现。
测试数据管理:独立账号+接口造数
测试数据是UI自动化的隐形杀手。每个用例需要不同状态的订单:待接单、配送中、已签收。如果直接操作UI去造数据,一个用例跑完得几分钟,还不稳定。
我的方案是接口造数:在用例执行前,调用后端API快速创建指定状态的订单。后端提供了测试接口,我用Python的requests在setup里调用:
import requests def create_order(status): resp = requests.post( 'http://test.api.express.com/order/create', json={'status': status, 'driver_id': 'test_driver'} ) assert resp.status_code == 200 return resp.json()['order_id']账号体系用独立的测试账号,每个用例跑完清理数据,保证可重复执行。数据清理也是通过API删除订单,避免垃圾数据堆积。
核心洞见:UI自动化只负责UI操作,数据准备和清理交给API层。这才符合自动化金字塔的原则,速度和稳定性都能兼顾。
接入Jenkins:凌晨自动跑,早上看报告
Jenkins配置不算复杂,但有几个细节值得注意。我建了一个pipeline任务,使用Jenkinsfile管理构建步骤:
pipeline { agent any triggers { cron('0 2 * * *') // 每天凌晨2点跑 } stages { stage('启动Appium') { steps { sh 'appium --address 127.0.0.1 --port 4723 &' } } stage('启动模拟器') { steps { sh 'emulator -avd test_avd -no-window &' } } stage('执行测试') { steps { sh 'pytest tests/ --html=report.html --self-contained-html' } } stage('发布报告') { steps { publishHTML([ reportDir: 'report', reportFiles: 'report.html', reportName: 'UI自动化测试报告' ]) } } } post { always { cleanWs() } } }坑点:模拟器启动需要时间,所以我在执行测试前加了等待设备就绪的步骤,用adb wait-for-device。
报告用pytest-html插件,在Jenkins上通过HTML Publisher展示,每天早上团队直接看链接,失败了点进去看截图和日志。
效果与经验总结
这套体系上线后,核心流程的回归时间从一天缩到30分钟。我统计过一个月的运行数据:共跑了30次,通过率88%,失败主要集中在App版本更新导致的元素变动,平均每次修复耗时不超过20分钟。
几点经验供参考:
- 环境一定要容器化或脚本化,我写了个setup.sh,一键装好所有依赖,新同事半天上手。
- 元素定位多用稳定属性,比如resource-id、text,少用XPath索引。
- 数据独立是自动化的生命线,别在共享环境里跑,否则互相干扰。
- CI不是终点,失败后的结果分析流程更重要,我做了失败截图自动上传到内部IM机器人,省去很多沟通成本。
UI自动化不是万能药,它适合核心流程和稳定版本,但对于频繁变动的业务,维护成本会很高。我的建议是:先选5个最核心的流程做,跑顺了再扩展,别一上来就铺开。
如果你们团队也正在发愁测试效率,或许可以从这套方案里找到切入点。我们铭锦数智也提供自动化测试的定制服务,有需要可以聊聊。