1. 这不是“装个Appium就能跑脚本”的事而是构建一套可维护、可扩展、能进CI的工程化能力很多人点开“Appium Android自动化教程”时心里想的是“我只要能启动一个APP点两下按钮截图验证一下就算入门了。”结果真动手才发现Appium服务起不来、设备连不上、元素定位总失败、脚本一换手机就报错、CI上跑着跑着就卡死……最后项目里积压了20个test.py文件但没人敢动因为改一行全链路就崩。这不是自动化这是“自动添堵”。我带过6个不同行业的Appium落地项目从金融类高安全要求的银行APP到电商类强交互的直播购物应用再到IoT配套的蓝牙控制面板踩过的坑基本覆盖了Android自动化所有典型断点。Appium本身只是个通信桥真正决定成败的是它背后那套支撑“稳定识别—可靠执行—精准断言—快速反馈”的工程骨架。这个骨架不靠Appium文档生成而靠你对Android系统机制、ADB底层行为、UIAutomator2引擎原理、以及测试生命周期管理的真实理解。这篇内容就是帮你把这套骨架亲手搭出来。它不叫“Appium入门”它叫“Android端到端自动化交付能力构建实录”。你会看到为什么必须用uiautomator2而不是默认的appium-uiautomator2-driver为什么adb shell getprop ro.build.version.release的结果比deviceName更重要为什么一个看似简单的find_element(By.ID, com.xxx:id/login_btn)在小米和华为上会返回完全不同的响应结构这些不是配置问题是Android碎片化生态下的必然现象而我们要做的是把“必然”变成“可控”。适合谁看如果你已经能写几行Python调用Appium但每次升级Android SDK或换新机型都要重调3天如果你的测试报告里还写着“手动截图比对”如果你的CI流水线里自动化用例长期处于“跳过”状态——那你不是缺教程是缺一套经得起真实业务压力检验的框架设计逻辑。接下来的内容每一节都对应一个真实交付场景中的关键决策点没有废话全是我在产线环境里反复验证过的硬核细节。2. 环境不是“装完就完”而是要建立三层隔离与版本锚定机制很多教程一上来就让你pip install Appium-Python-Client然后appium -v再adb devices——看起来很顺但实际项目里这三步背后藏着至少5个隐性风险点。我见过最典型的案例团队在Mac上开发调试一切正常推到Linux CI服务器后所有基于AccessibilityNodeInfo的定位全部失效查了两天才发现是Java版本不一致导致UIAutomator2编译产物ABI不兼容。所以环境搭建的第一原则不是“能跑”而是“可复现、可迁移、可审计”。2.1 Java与Android SDK必须锁定主版本号而非“最新版”Appium依赖Java运行时JRE和Android SDK工具链adb、aapt、uiautomator等但这两者之间存在严格的版本契约。例如Appium 2.4.0 要求 Java 11 或 17官方明确不支持Java 21uiautomator2驱动要求 Android SDK Build-Tools ≥ 30.0.3但 ≤ 34.0.034.0.1起移除了aapt的--no-version-vectors参数导致资源解析失败platform-tools含adb必须与目标设备Android版本匹配Android 13设备若用adb 33.x会出现adb shell input tap坐标偏移问题提示不要用sdkmanager --install platform-tools这种模糊命令。必须显式指定版本sdkmanager platform-tools;33.0.2sdkmanager build-tools;30.0.3sdkmanager platforms;android-33所有版本号需记录在项目根目录的env/SDK_VERSIONS.md中CI脚本直接读取该文件执行安装。我在线上环境强制推行“三版本锁”策略JavaJAVA_HOME指向/opt/java/jdk-17.0.2、Android SDKANDROID_HOME指向/opt/android-sdk内含固定版本的platform-tools/build-tools/platforms、Node.jsAppium Server运行环境固定为v18.17.0。每次CI构建前先校验java -version、adb version、node -v三者输出是否与锁文件一致不一致则中断构建。这个动作看似繁琐却避免了90%以上的“本地能跑线上挂”的诡异问题。2.2 Appium Server放弃全局安装改用Docker容器化部署Appium Server本身是个Node.js服务全局安装npm install -g appium会导致多个项目共享同一套driver和日志配置一旦某个项目升级appium-uiautomator2-driver其他项目可能因API变更而崩溃。更严重的是全局安装无法实现多版本并存——比如你同时要支持Android 10需uiautomator2 v4.12.0和Android 14需v4.25.0全局安装根本做不到。解决方案用Docker封装Appium Server。我们维护了一个内部镜像our-registry/appium-server:2.4.0-uia2-v4.25.0其Dockerfile核心逻辑如下FROM appium/appium:2.4.0 # 卸载默认driver RUN appium driver uninstall uiautomator2 # 安装指定版本driver源码编译确保ABI兼容 RUN appium driver install --source local /tmp/uiautomator2-v4.25.0.tgz # 挂载自定义日志配置和设备映射 VOLUME [/var/log/appium] EXPOSE 4723 CMD [appium, --allow-insecureadb_shell, --relaxed-security]CI流水线中启动命令为docker run -d \ --name appium-server \ -p 4723:4723 \ -v $(pwd)/logs:/var/log/appium \ -v /dev/bus/usb:/dev/bus/usb \ our-registry/appium-server:2.4.0-uia2-v4.25.0这个设计带来三个确定性第一每个项目独占Server实例driver版本互不干扰第二日志路径统一挂载便于集中采集分析第三USB设备直通支持真机集群调度。我们曾用该方案在单台物理机上稳定运行7个Appium容器分别对接7种不同品牌/Android版本的测试机连续运行18个月无一次因Server侧异常导致用例失败。2.3 Python客户端与依赖用Poetry管理而非requirements.txtAppium-Python-Client只是协议封装层它背后依赖selenium、urllib3、certifi等库而这些库的版本组合直接影响HTTP请求稳定性。例如urllib32.0.0会强制校验SSL证书若Appium Server使用自签名证书CI内网常见就会抛出MaxRetryError。用pip install -r requirements.txt无法解决这种传递依赖冲突。我们切换到Poetrypyproject.toml关键片段如下[tool.poetry.dependencies] python ^3.9 Appium-Python-Client { version ^2.12.0, allow-prereleases false } selenium { version ^4.11.2, allow-prereleases false } urllib3 { version ^1.26.15, allow-prereleases false } # 锁死1.x分支 certifi ^2023.7.22 [tool.poetry.group.dev.dependencies] pytest ^7.4.0 allure-pytest ^2.12.0Poetry的poetry lock会生成精确的poetry.lock文件记录每个包的SHA256哈希值。CI中执行poetry install时会严格校验哈希确保安装的每一个字节都与开发环境完全一致。我们曾遇到一个诡异问题某次pip install后urllib3被升级到2.0.7导致所有HTTPS请求超时排查了6小时才发现是requests库的隐式依赖更新所致。换成Poetry后此类问题归零。注意Appium-Python-Client的2.12.0版本已修复find_elements在空列表时返回None的bug旧版返回空list新版统一为list这个细节直接影响你的断言写法必须在pyproject.toml中显式锁定。3. 设备连接不是“adb devices有显示就行”而是要建立设备健康度实时监控体系adb devices输出List of devices attached下面跟着一行0123456789ABCDEF device很多人就认为设备“连上了”。但真实场景中这行输出背后可能隐藏着adb daemon内存泄漏导致响应延迟、设备USB供电不足引发ADB断连、厂商定制ROM屏蔽了adb shell dumpsys window windows命令、甚至设备正在后台执行OTA升级而拒绝任何shell指令。这些情况不会让adb devices报错但会让Appium在create_session阶段卡死90秒后超时。3.1 设备初始化检查清单12项硬性指标必须全部通过我们在每个测试用例执行前插入一个device_health_check()函数它会依次执行以下检查全部通过才允许后续操作检查项命令合格标准失败后果1. ADB连接稳定性adb -s $UDID shell echo ok×3次3次均返回ok平均耗时800ms触发设备重连流程2. 系统服务可用性adb -s $UDID shell getprop init.svc.bootanim返回stopped动画结束等待至stopped或超时弃用3. UIAutomator进程存活adb -s $UDID shell ps | grep uiautomator进程存在且PPID为1自动adb shell am force-stop com.github.uiautomator后重启4. 屏幕点亮状态adb -s $UDID shell dumpsys power | grep mScreenOntrue匹配成功执行adb shell input keyevent KEYCODE_WAKEUP5. 输入法禁用adb -s $UDID shell settings get secure default_input_method返回空或null切换至系统默认输入法6. 开发者选项启用adb -s $UDID shell settings get global adb_enabled返回1自动开启需提前授权7. USB调试授权adb -s $UDID shell getprop service.adb.root返回1若为0触发adb root重试8. 存储空间余量adb -s $UDID shell df /data | tail -1 | awk {print $4}512MB清理/data/local/tmp缓存9. 时间同步状态adb -s $UDID shell date与宿主机时间差5s执行adb shell date $(date %m%d%H%M%Y.%S)10. 网络连通性adb -s $UDID shell ping -c 1 -W 1 114.114.114.114time字段存在切换至Wi-Fi或忽略网络用例11. 电池电量adb -s $UDID shell dumpsys battery | grep levellevel:后数值≥30暂停执行进入充电队列12. 设备温度adb -s $UDID shell cat /sys/class/thermal/thermal_zone*/temp 2/dev/null | head -145000单位毫摄氏度强制降温暂停10分钟这个检查清单不是理论设计而是我们从37台不同品牌设备华为、小米、OPPO、vivo、三星、Pixel、OnePlus等的217次失败用例中反向归纳出来的。例如第9项“时间同步”曾导致某银行APP的登录Token签名验证失败——因为设备时间慢了3分17秒JWT的exp字段已过期但Appium日志只显示SessionNotCreatedException根本看不出根源。现在只要时间偏差超标检查函数会自动校准并在Allure报告中标红记录“Time sync corrected: -197s”。3.2 真机集群调度用AdbController实现设备状态感知与智能分配当设备数量超过5台手动管理adb devices输出变得不可持续。我们开发了一个轻量级AdbController类它不依赖第三方框架仅用Python标准库ADB命令实现class AdbController: def __init__(self, device_pool: List[str]): self.pool device_pool # [0123456789ABCDEF, FEDCBA9876543210] self.status {} # {udid: {health_score: 87, last_used: 2023-10-01 14:22:03}} def get_best_device(self, min_health: int 70) - Optional[str]: candidates [ udid for udid in self.pool if self.status.get(udid, {}).get(health_score, 0) min_health ] if not candidates: return None # 优先选择最近未被使用的设备减少热机老化 return min(candidates, keylambda x: self.status.get(x, {}).get(last_used, )) def update_health(self, udid: str, score: int): self.status[udid] { health_score: score, last_used: datetime.now().strftime(%Y-%m-%d %H:%M:%S) } def run_adb_cmd(self, udid: str, cmd: str) - str: # 封装adb命令自动重试3次失败时更新health_score try: result subprocess.run( fadb -s {udid} {cmd}, shellTrue, capture_outputTrue, textTrue, timeout30 ) if result.returncode 0: self.update_health(udid, min(100, self.status.get(udid, {}).get(health_score, 100) 5)) return result.stdout.strip() else: self.update_health(udid, max(0, self.status.get(udid, {}).get(health_score, 100) - 20)) raise Exception(fADB failed: {result.stderr}) except Exception as e: self.update_health(udid, max(0, self.status.get(udid, {}).get(health_score, 100) - 30)) raise e在测试基类中我们这样调用class BaseTestCase(unittest.TestCase): def setUp(self): controller AdbController([0123456789ABCDEF, FEDCBA9876543210]) self.udid controller.get_best_device() if not self.udid: raise RuntimeError(No healthy device available) self.driver webdriver.Remote( http://localhost:4723/wd/hub, { platformName: Android, deviceName: self.udid, udid: self.udid, appPackage: com.example.app, appActivity: .MainActivity, noReset: True, newCommandTimeout: 120 } )这套机制上线后设备平均健康分从61提升至89单日设备故障率下降76%。最关键的是它让“设备管理”从运维黑盒变成了可量化、可预测的工程环节——你可以清晰看到每台设备的健康趋势图提前更换老化设备而不是等它在凌晨三点的CI任务中突然掉线。4. 元素定位不是“用ID最简单”而是要构建多层级、可降级、带上下文感知的定位策略树By.ID确实写起来最短但把它当作首选方案是Android自动化项目夭折的最主要原因。Android的ID机制本质是R.java生成的int常量一旦APK启用Resource ShrinkingProGuard/R8默认开启未被代码引用的ID会被彻底删除。我们曾接手一个电商APP其登录按钮ID为id/btn_login但在Release包中该ID不存在find_element(By.ID, btn_login)永远抛NoSuchElementException。更隐蔽的是某些厂商ROM会动态修改View ID如小米MIUI的“安全中心”页面所有控件ID末尾自动追加随机字符串导致ID定位完全失效。4.1 定位策略树五层降级机制确保任意场景都有兜底方案我们设计了一个SmartLocator类它按优先级顺序尝试5种定位方式任一成功即返回元素失败则自动降级class SmartLocator: def __init__(self, driver: WebDriver): self.driver driver self.strategy_order [ self._by_accessibility_id, # 第一层content-desc人工可读稳定 self._by_xpath_text, # 第二层//android.widget.Button[text登录] self._by_xpath_desc, # 第三层//android.widget.Button[contains(content-desc, 登录)] self._by_class_and_index, # 第四层//android.widget.Button[1]慎用需结合上下文 self._by_image_match, # 第五层OpenCV图像识别终极兜底 ] def find(self, target: str, timeout: int 10) - WebElement: for strategy in self.strategy_order: try: element strategy(target) if element: # 记录本次成功策略用于后续同类元素优化 self._log_strategy_success(target, strategy.__name__) return element except Exception as e: continue raise NoSuchElementException(fCould not locate element: {target}) def _by_accessibility_id(self, target: str) - Optional[WebElement]: # 优先尝试content-desc这是Android官方推荐的无障碍定位方式 try: return self.driver.find_element(By.ACCESSIBILITY_ID, target) except: return None def _by_xpath_text(self, target: str) - Optional[WebElement]: # text属性最稳定但需注意中文字符需转义且区分大小写 xpath f//*[text{target.replace(\\, \\\\)} or text{target.upper()} or text{target.lower()}] try: return self.driver.find_element(By.XPATH, xpath) except: return None def _by_xpath_desc(self, target: str) - Optional[WebElement]: # content-desc可能包含冗余信息用contains模糊匹配 xpath f//*[content-desc[contains(., {target})]] try: return self.driver.find_element(By.XPATH, xpath) except: return None def _by_class_and_index(self, target: str) - Optional[WebElement]: # 作为最后手段用类名索引但必须结合当前页面上下文过滤 # 例如先定位到登录页根布局再在其子节点中找第一个Button try: root self.driver.find_element(By.CLASS_NAME, android.widget.LinearLayout) buttons root.find_elements(By.CLASS_NAME, android.widget.Button) if buttons: return buttons[0] except: pass return None def _by_image_match(self, target: str) - Optional[WebElement]: # 终极兜底截取当前屏幕用OpenCV模板匹配目标图片 # 需提前准备target对应的PNG截图如login_btn.png screenshot self.driver.get_screenshot_as_png() template cv2.imread(ftemplates/{target}.png, 0) img_gray cv2.cvtColor(cv2.imdecode(np.frombuffer(screenshot, np.uint8), cv2.IMREAD_COLOR), cv2.COLOR_BGR2GRAY) res cv2.matchTemplate(img_gray, template, cv2.TM_CCOEFF_NORMED) loc np.where(res 0.8) # 匹配阈值0.8 if len(loc[0]) 0: # 计算中心坐标并转换为Appium可点击的Point pt (loc[1][0] template.shape[1]//2, loc[0][0] template.shape[0]//2) return self._point_to_element(pt) return None这个策略树的价值在于它把“定位失败”从异常事件变成了可管理的流程。当某次_by_accessibility_id失败时日志会记录[STRATEGY_FALLBACK] login_btn - xpath_text你立刻知道是content-desc缺失当连续3次降级到_by_image_match说明该页面的UI结构极不稳定需要推动开发补充无障碍属性。我们统计过在237个核心业务流程中92%的元素首次定位即成功_by_accessibility_id或_by_xpath_text剩余8%中7%通过二次降级解决仅1%需要图像识别——而这1%恰恰是那些“弹窗广告”“活动Banner”等无法用代码控制的野区图像识别反而是最鲁棒的方案。4.2 上下文感知用Page Object ModelPOM封装页面状态机单纯封装元素定位还不够必须把页面逻辑也纳入管理。我们摒弃了传统POM中“每个页面一个类”的粗粒度设计改为“每个页面状态一个类”因为Android Activity/Fragment的生命周期决定了同一个Activity类在不同状态下其DOM结构可能完全不同。以登录页为例它存在三种状态LoginState.UNINITIALIZED刚启动加载中只有ProgressBarLoginState.READY输入框、密码框、登录按钮可见LoginState.ERROR密码错误提示Toast出现登录按钮置灰对应的POM设计如下class LoginPage: def __init__(self, driver: WebDriver): self.driver driver self.locator SmartLocator(driver) def wait_for_state(self, state: str, timeout: int 15) - bool: 等待页面进入指定状态超时返回False start time.time() while time.time() - start timeout: try: if state UNINITIALIZED: self.locator.find(loading, timeout1) return True elif state READY: self.locator.find(username_input, timeout1) self.locator.find(password_input, timeout1) self.locator.find(login_btn, timeout1) return True elif state ERROR: # Toast是系统级弹窗需特殊处理 toast self.driver.find_element(By.XPATH, //*[classandroid.widget.Toast]) if toast and 密码错误 in toast.text: return True except: pass time.sleep(0.5) return False def enter_username(self, username: str): if not self.wait_for_state(READY): raise RuntimeError(LoginPage not ready) elem self.locator.find(username_input) elem.clear() elem.send_keys(username) def click_login(self): if not self.wait_for_state(READY): raise RuntimeError(LoginPage not ready) self.locator.find(login_btn).click() # 点击后预期状态变为UNINITIALIZED加载中 if not self.wait_for_state(UNINITIALIZED, timeout5): raise RuntimeError(Login submit failed) # 使用示例 page LoginPage(driver) page.enter_username(testuser) page.click_login() # 此时页面应处于加载中状态等待跳转 assert page.wait_for_state(UNINITIALIZED)这种状态机POM带来的最大收益是用例代码不再关心“当前页面长什么样”只关心“我要做什么然后等待什么结果”。以前写一个登录用例要写12行代码判断各种元素是否存在现在只需3行page.enter_username()、page.click_login()、assert page.wait_for_state(...)。我们重构了152个老用例平均代码行数减少64%可读性提升300%更重要的是当UI改版时只需修改LoginPage类中的状态检测逻辑所有调用它的用例自动适配——这才是框架该有的样子。5. 测试执行不是“跑完就完”而是要构建带根因分析的失败诊断闭环自动化测试最大的价值不是“证明它能过”而是“当它失败时告诉我为什么”。但现实是90%的Appium失败日志只有一行NoSuchElementException然后测试工程师就要花2小时去真机上手动复现再逐行调试。我们必须把“失败分析”变成自动化的一部分让每次失败都自带诊断报告。5.1 失败快照三件套屏幕截图XML DOMLogcat日志三位一体关联存储Appium原生的screenshot()只能保存图片但一张图解决不了问题。我们重写了take_failure_snapshot()方法它在捕获异常的瞬间同步执行三项操作屏幕截图driver.get_screenshot_as_file(fsnapshots/{test_name}_{timestamp}_screen.png)当前页面DOMdriver.page_source保存为{test_name}_{timestamp}_dom.xml关键Logcat日志adb -s $UDID logcat -t 100 -v threadtime *:S ActivityManager:I WindowManager:I保存为{test_name}_{timestamp}_logcat.log这三份文件用相同的时间戳命名并在Allure报告中作为附件绑定到失败用例下。当查看报告时你可以同时打开截图、DOM树和日志进行交叉验证。例如截图显示登录按钮是灰色的DOM中发现Button ... enabledfalseLogcat里找到E/LoginActivity: Password validation failed——三者印证根因立刻清晰。提示Logcat过滤策略至关重要。我们不用*:S屏蔽所有再开白名单而是用*:S ActivityManager:I WindowManager:I即只保留系统关键服务的日志。实测发现全量Logcat在Android 13上每秒产生12MB日志而我们的过滤策略将体积压缩到0.3MB/秒且100%覆盖了Activity跳转、窗口状态、ANR等关键事件。5.2 根因分类引擎用规则匹配自动标注失败类型有了三件套数据下一步是自动分类。我们构建了一个轻量级FailureClassifier它基于预设规则对失败进行打标class FailureClassifier: RULES [ # 规则如果DOM中存在android:id/progress且enabledtrue截图中ProgressBar可见则标记为LOADING_TIMEOUT (LOADING_TIMEOUT, lambda dom, screen, log: ProgressBar in dom and enabledtrue in dom and self._is_progress_bar_visible(screen)), # 规则如果Logcat中出现ANR in且DOM为空则标记为APP_CRASHED (APP_CRASHED, lambda dom, screen, log: ANR in in log and hierarchy not in dom), # 规则如果DOM中找不到目标元素且Logcat显示Could not identify the current activity则标记为ACTIVITY_NOT_RESUMED (ACTIVITY_NOT_RESUMED, lambda dom, screen, log: NoSuchElementException in log and Could not identify the current activity in log), # 规则如果截图中文字区域OCR识别出网络连接异常则标记为NETWORK_ERROR (NETWORK_ERROR, lambda dom, screen, log: self._ocr_contains(screen, 网络连接异常)), ] def classify(self, dom_path: str, screen_path: str, log_path: str) - str: with open(dom_path) as f: dom f.read() with open(log_path) as f: log f.read() screen cv2.imread(screen_path) for label, rule in self.RULES: try: if rule(dom, screen, log): return label except: continue return UNKNOWN # 在测试tearDown中调用 def tearDown(self): if self._test_has_failed(): snapshot self.take_failure_snapshot() classifier FailureClassifier() root_cause classifier.classify( f{snapshot[dom_path]}, f{snapshot[screen_path]}, f{snapshot[log_path]} ) # 将root_cause写入Allure标签 allure.dynamic.tag(root_cause)上线三个月后我们收集了1,247次失败记录其中LOADING_TIMEOUT占38%主要因网络抖动或后端响应慢APP_CRASHED占22%集中在特定Android 12 OEM ROMACTIVITY_NOT_RESUMED占19%因测试脚本未等待Activity完全Resume就执行操作NETWORK_ERROR占12%UNKNOWN仅占9%这个数据直接驱动了改进针对LOADING_TIMEOUT我们增加了wait_for_activity_resume()钩子针对APP_CRASHED我们为Android 12设备单独配置了appWaitDuration参数针对ACTIVITY_NOT_RESUMED我们强制所有页面操作前调用self.driver.current_activity校验。自动化测试的价值正在于它能把模糊的“又挂了”变成精确的“38%是加载超时该优化网络等待策略”。5.3 自愈机制对可预测失败自动执行修复动作有些失败是可预测、可修复的没必要让测试停止。我们实现了两级自愈一级自愈轻量级检测到StaleElementReferenceException元素过期自动重新定位该元素最多重试3次检测到WebDriverException含timeout自动增加implicitly_wait至15秒重试当前操作检测到ElementNotInteractableException执行element.location_once_scrolled_into_view后重试二级自愈重量级检测到连续3次LOADING_TIMEOUT触发adb shell input keyevent KEYCODE_BACK返回上一页再重新导航至当前页检测到APP_CRASHED执行adb shell am force-stop com.example.app然后adb shell am start -n com.example.app/.SplashActivity重启APP检测到ACTIVITY_NOT_RESUMED执行adb shell input keyevent KEYCODE_HOME再adb shell am start -n com.example.app/.CurrentActivity自愈逻辑嵌入在SmartLocator和BaseTestCase中对用例代码完全透明。上线后整体用例失败率下降41%其中32%的失败被一级自愈消化9%被二级自愈恢复。最典型的是电商APP的“商品详情页”因图片懒加载策略在弱网下经常LOADING_TIMEOUT现在它会自动回退再进入成功率从67%提升至99.2%。6. 框架不是“写完就扔”而是要沉淀为可复用、可审计、可演进的组织资产一个自动化框架如果只服务于当前项目它的生命周期不会超过6个月。真正的工程化框架必须能跨项目复用、接受安全审计、并支持技术演进。我们花了8个月把这套Appium实践沉淀为公司级android-automation-framework它已支撑12个业务线、47个APP的日常回归。6.1 可复用性设计用插件化架构解耦核心能力与业务逻辑框架采用“核心引擎插件”的分层设计android-automation-framework/ ├── core/ # 不变的核心设备管理、Driver封装、失败诊断 │ ├── device/ │ ├── driver/ │ └── failure/ ├── plugins/ # 可插拔的业务能力 │ ├── biometric/ # 生物识别模拟指纹/人脸 │ ├── network/ # 网络条件模拟弱网/断网 │ ├── mock/ # 接口Mock基于adb reverse │ └── report/ # 报告增强Allure自定义指标 └── templates/ # 项目模板一键生成新项目脚手架 └── basic/新项目只需执行cookiecutter https://gitlab.com/our-org/android-automation-framework/templates/basic cd my-new-project poetry install # 自动集成core和所需plugins所有插件都遵循统一接口规范例如NetworkPlugin必须实现class NetworkPlugin(ABC): abstractmethod def set_profile(self, profile: str): # 3g, 4g, wifi, offline pass abstractmethod def reset(self): pass当某业务线需要测试弱网场景只需在pyproject.toml中添加[tool.poetry.dependencies] android-automation-framework-network ^1.2.0然后在测试代码中from android_automation_framework.plugins.network import NetworkPlugin plugin NetworkPlugin(driver) plugin.set_profile(3g) # 执行测试... plugin.reset()这种设计让框架具备了“乐高式”组装能力。我们曾用同一套core为支付线集成了biometric
相关文章:
大模型内卷结束,Agent 正规军围剿“PPT大师”
g1000,TS9020,g3810,G5080,ts5480,G7080,MG3680,G3800,G2800,报错5B00,P07,5b02,1700,1704,5b04废墨垫清零软件,亲测有效
PID+Smith预测器:驯服微波炉碳纤维固化的延迟与非线性
长春市崇文高中——宿舍育人
3步掌握YOLOv5_OBB:从零开始构建旋转目标检测模型
Prithvi-EO-2.0:时空感知遥感基础模型原理、实战与避坑指南
FactoryBluePrints黑雾防御系统完全指南:从基础防护到高效资源管理
【k8s部署】
ThinkPad黑苹果系统架构探索:从硬件兼容到macOS生态的完整实现路径
矢量数据 SHP 常见几何类型
AI Agent 不只会聊天:用 OpenClaw Skill 把常用工具变成可调用工作流
AI Agent无代码开发全栈路径(从Prompt编排到生产上线全流程拆解)
3步掌握Vin象棋:基于YOLOv5的智能象棋连线工具终极指南
等保2.0三级必考题:为什么“物理隔离”是唯一的满分答案?
2026国产液体涡轮流量计十大品牌排名深度解析:技术实力与选型实战指南 - 仪表品牌排行榜
安防设备与交通设备线上推广怎么做?双赛道企业如何一箭双雕 - 品牌推荐大师1
为什么选择Real-ESRGAN:3个核心优势解决你的图像修复难题
IronyModManager终极指南:Paradox游戏模组冲突解决与智能管理
有什么好的中小企业日常在线考试系统?试试轻量化神器麦塔!
2026 App开发技术全景解析:从框架选型到AI融合新趋势
别再硬啃长资料了:用 GPT-5.5 + 脑图工具,30 分钟梳理复杂主题
企业级开源MES系统:基于ISA标准的制造业数字化转型完整解决方案
5步掌握QQ音乐解析:新手快速上手指南
最近我把 Mac 上的截图工具换成了 Snapzy。
C#零依赖实现高精度鼠标宏:基于事件流重演的稳定自动化方案
3步高效实现微信QQ消息防撤回:RevokeMsgPatcher完整实用指南
买完物联网平台才发现:这玩意儿改不动啊!
大窗标杆品牌,行业率先提供大窗系统解决方案的品牌
多标签局部判别嵌入(MLDE):破解高维多标签分类的降维难题
2025年底大润发卡回收价格表|实体卡、电子卡最新行情对比 - 可可收公众号