iOS自动化测试实战:基于facebook-wda的轻量级方案与核心技巧

iOS自动化测试实战:基于facebook-wda的轻量级方案与核心技巧
1. 项目概述为什么选择 facebook-wda 进行 iOS 自动化测试如果你是一名移动端测试工程师或者正在尝试为你的 iOS 应用构建一套自动化回归测试脚本那么你很可能已经听说过 Appium。Appium 作为一款老牌的跨平台自动化测试框架功能强大但架构相对复杂尤其是在 iOS 端它依赖于 WebDriverAgentWDA这个“中间人”来与设备通信。而facebook-wda则是一个更轻量、更 Pythonic 的选择它直接通过 HTTP 协议与 WDA 通信绕过了 Appium Server 这一层让脚本编写和调试都变得更加直接和高效。简单来说facebook-wda 是一个 Python 客户端库它让你能够用纯 Python 代码来驱动 iOS 设备完成点击、滑动、输入、获取元素属性等一系列 UI 操作。它的核心优势在于“直接”。你不再需要处理 Appium 复杂的 Desired Capabilities 配置也不需要在本地启动一个 Appium Server。你只需要确保你的 iPhone 或 iPad 上运行着 WDA 服务然后就可以像调用本地函数一样通过 facebook-wda 发送指令。这对于追求脚本执行效率和简洁代码风格的同学来说吸引力巨大。我最初从 Appium 转向 facebook-wda是因为在一个需要快速验证大量 UI 交互场景的项目中Appium 的启动和会话建立时间成了瓶颈。facebook-wda 的直连模式让我节省了近 30% 的脚本初始化时间。更重要的是它的 API 设计非常直观错误信息也更清晰当定位失败或操作超时时我能更快地定位到问题根源而不是在 Appium 繁杂的日志中大海捞针。这篇文章我将带你从零开始在 10 分钟内快速上手 facebook-wda。无论你是自动化测试新手还是想寻找更优 iOS 自动化方案的资深测试这篇指南都将提供一条清晰的路径。我们将涵盖环境搭建、核心 API 使用、元素定位技巧以及一个完整的实战案例让你能立刻将所学应用到自己的项目中。2. 环境准备与核心组件解析在开始编写第一行代码之前我们需要搭建好整个自动化测试的“舞台”。这个舞台的核心是WebDriverAgent (WDA)和facebook-wda库。理解它们各自扮演的角色是后续顺利操作的关键。2.1 核心组件WebDriverAgent (WDA) 是什么你可以把 WDA 想象成安装在 iOS 设备上的一个“遥控接收器”。它是一个由 Facebook 开源现在由 Appium 社区维护的 iOS 应用实现了 WebDriver 协议。这个协议定义了一套标准允许外部程序比如我们的测试脚本通过 HTTP 请求来远程控制设备。WDA 的核心作用UI 访问提供接口来获取当前屏幕的视图层级结构类似于 Android 的 UI Automator。执行操作接收点击、滑动、输入等指令并转化为设备上的实际动作。状态反馈返回操作结果、元素属性、设备信息等。没有 WDA任何外部的 iOS 自动化工具包括 Appium都无法工作。因此我们的第一步就是让 WDA 在设备上跑起来。2.2 环境搭建三种启动 WDA 的方式根据你手头的设备Mac、Windows、Linux和 iOS 设备真机或模拟器启动 WDA 的方式略有不同。这里我重点介绍最通用、对平台限制最少的一种方式使用tidevice。为什么推荐 tidevicetidevice 是一个纯 Python 编写的工具它完美解决了在非 Mac 系统如 Windows 或 Linux上管理 iOS 设备包括安装应用、启动 WDA、获取设备信息等的难题。这意味着你完全可以在 Windows 电脑上对 iPhone 进行自动化测试这大大降低了环境门槛。步骤 1安装 tidevice在你的电脑Windows/Mac/Linux均可上打开命令行使用 pip 安装pip install -U tidevice步骤 2连接设备并安装 WDA用 USB 数据线将你的 iOS 设备连接到电脑。信任此电脑如果首次连接。在命令行中列出设备tidevice list你会看到类似输出记住你的设备 UDIDList of apple devices attached 00008101-000255021E08001E iPhone12安装 WDA 的 .ipa 文件到设备。你需要先获取 WDA 的编译产物。对于真机测试最方便的方式是从可靠的渠道如 Appium 官方仓库的 Releases 页面下载已使用有效开发者证书签名的 WDA.ipa 文件。假设你已下载WebDriverAgent.ipa到当前目录。tidevice -u 00008101-000255021E08001E install WebDriverAgent.ipa步骤 3启动 WDA 服务安装成功后使用以下命令启动 WDA 并映射端口到本地tidevice -u 00008101-000255021E08001E wdaproxy -B com.facebook.WebDriverAgent.test.xctrunner --port 8100-u: 指定设备 UDID。-B: 指定 WDA 应用的 Bundle ID。这是 WDA 应用在设备上的唯一标识通常为com.facebook.WebDriverAgent.test.xctrunner。--port: 将设备上的 WDA 服务端口默认 8100映射到本机的 8100 端口。看到类似[I 220101 10:00:00 _wdaproxy:90] WDA start successfully的日志说明 WDA 服务已成功启动。现在你可以在浏览器中访问http://localhost:8100/status来验证服务是否正常。如果返回一个包含state: success的 JSON恭喜你WDA 已就绪。注意首次在真机上启动 WDA需要在设备上手动信任开发者证书进入“设置”-“通用”-“设备管理”或“VPN与设备管理”找到你的开发者证书并信任。否则 WDA 应用将无法启动。2.3 安装 facebook-wda 与辅助工具WDA 服务跑起来后我们就可以安装“遥控器”——facebook-wda 库了。pip install -U facebook-wda可选但强烈推荐安装 Weditor 进行元素定位Weditor 是一个基于 Web 的 UI 元素查看器类似于 Appium Desktop 的 Inspector。它能直观地展示当前屏幕的视图层级并帮你生成定位表达式。pip install -U weditor安装后在命令行输入weditor它会自动打开浏览器。在界面中选择 “iOS”输入设备地址如http://localhost:8100即可实时查看和定位元素。务必在启动 WDA 服务后再使用 Weditor否则无法连接。至此你的自动化测试环境已经全部准备完毕。总结一下核心链路你的 Python 脚本 (facebook-wda) - HTTP 请求 - 本机 8100 端口 (tidevice 代理) - USB/网络 - iOS 设备上的 WDA 应用 - 操控设备 UI。3. 快速入门你的第一个自动化脚本理论说再多不如动手跑一遍。让我们从一个最简单的脚本开始目标是打开 iOS 的“设置”应用并获取当前应用的 Bundle ID。3.1 初始化连接与基础操作创建一个新的 Python 文件例如first_test.py写入以下代码import wda # 1. 创建客户端连接 # 方式一使用默认的本地8100端口推荐与上面启动的wdaproxy对应 c wda.Client(http://localhost:8100) # 方式二如果不传参数默认也是 http://localhost:8100 # c wda.Client() # 2. 等待WDA服务就绪重要 c.wait_ready(timeout120) # 3. 打印设备信息 print(设备信息:, c.device_info()) print(屏幕分辨率:, c.window_size()) print(当前应用:, c.app_current()) # 4. 一些基础设备操作 print(屏幕是否锁定:, c.locked()) c.home() # 按一次Home键确保回到主屏幕 c.unlock() # 解锁屏幕如果已锁定运行这个脚本 (python first_test.py)你应该能在控制台看到设备型号、屏幕分辨率以及当前前台应用的信息可能是com.apple.springboard即主屏幕。代码解析与避坑指南c.wait_ready(timeout120)这行代码至关重要。它会让脚本等待直到 WDA 服务完全启动并准备好接收命令。超时时间设置为 120 秒足以应对服务启动慢的情况。如果跳过这一步后续操作很可能因为服务未就绪而失败。c.home()和c.unlock()这些是全局设备操作不依赖于任何特定的应用会话。在开始测试一个 App 前先确保设备处于解锁状态并回到主屏幕是一个好习惯。常见错误如果连接失败请按顺序检查1) WDA 服务是否通过tidevice wdaproxy成功启动2) 设备 UDID 是否正确3) 端口 8100 是否被其他程序占用。3.2 启动、管理与关闭应用自动化测试的核心是与应用交互。facebook-wda 管理应用会话的方式非常简洁。import wda import time c wda.Client(http://localhost:8100) c.wait_ready(timeout30) # 启动一个应用例如“设置”其Bundle ID为 com.apple.Preferences # 这会创建一个会话 (session)后续操作都在这个会话上下文中进行 s c.session(com.apple.Preferences) # 或者使用 app_activate效果类似但语义上更偏向“激活”已存在的应用 # c.session().app_activate(com.apple.Preferences) time.sleep(2) # 等待应用完全启动 # 获取当前会话的应用状态 app_state s.app_state(com.apple.Preferences) print(f应用状态: {app_state}) # 状态值解释: 1-未运行 2-后台运行 4-前台运行 # 在应用内操作例如点击“通用”选项 # 这里我们先简单滑动一下确保界面加载完成 s.swipe_up() # 关闭应用 s.app_terminate(com.apple.Preferences) # 或者使用 s.close() 来关闭当前会话关键点c.session(bundle_id)这是启动/连接到某个应用的核心方法。它返回一个Session对象后续所有的元素查找和操作都基于这个对象。Bundle ID 如何获取对于系统应用可以网上搜索如设置是com.apple.Preferences。对于你自己的应用可以在 Xcode 项目设置或使用ideviceinstaller -l需额外安装命令查看。app_state在判断应用是否成功启动或是否在前台时非常有用。会话管理一个Client可以创建多个Session但同一时间通常只有一个活跃会话。关闭应用或创建新会话会自动管理之前的会话。4. 核心技能UI 元素定位与交互找到并操作屏幕上的元素是 UI 自动化的基本功。facebook-wda 提供了多种定位策略各有优劣。4.1 基本定位器最常用的方式定位元素的核心是使用s(定位表达式)其中s是你的Session对象。s c.session(com.apple.Preferences) s.implicitly_wait(10.0) # 设置隐式等待10秒全局生效 # 1. 通过 accessibility id (在iOS中通常对应name或label属性) # 这是最推荐的方式因为通常由开发同学设置且不易变。 s(name蓝牙).click() # 点击“蓝牙”选项 s(label蓝牙).click() # 与上面等价label是accessibilityLabel # 2. 通过类名 (className) # 类名描述了元素的类型如按钮、单元格、文本标签。 cells s(classNameXCUIElementTypeCell) # 找到所有类型为Cell的元素 if cells.exists: print(f找到了 {len(cells.find_elements())} 个单元格) # 3. 通过值 (value) # 例如开关控件、文本框的当前值。 switch s(value1) # 查找值为‘1’的元素可能是一个打开的开关 if switch.exists: print(找到了一个处于打开状态的开关) # 4. 组合条件定位 # 当单一属性无法唯一定位时可以组合使用。 s(classNameXCUIElementTypeCell, name蓝牙).click() # 这表示找到一个 className 为 XCUIElementTypeCell 且 name 为 ‘蓝牙’ 的元素。 # 5. 等待元素出现或消失 ele s(name蓝牙).wait(timeout5.0) # 显式等待5秒直到元素出现 s(name正在搜索...).wait_gone(timeout10.0) # 等待“正在搜索...”这个元素消失实操心得优先使用name/label这是最稳定、语义最清晰的定位方式。与开发团队约定好关键控件的accessibilityIdentifier能极大提升自动化脚本的健壮性。善用implicitly_wait设置一个合理的全局隐式等待时间如 10 秒可以让你的脚本在元素未立即出现时自动重试避免不必要的ElementNotFound错误。但注意它只对find类操作生效。existsvswaitexists是立即检查返回布尔值。wait是等待元素出现并返回元素对象如果超时未出现则抛出异常。根据场景选择使用。4.2 高级定位策略Predicate 与 Class Chain当基本定位器不够用时Predicate 和 Class Chain 是更强大的武器。它们由 iOS 原生框架支持执行效率高。Predicate 定位 使用 NSPredicate 格式的字符串进行筛选功能非常强大支持模糊匹配、比较、逻辑运算等。# 1. 字符串匹配 s(predicatename Wi-Fi).click() # 精确匹配 s(predicatename BEGINSWITH 蓝).click() # 开头是“蓝” s(predicatename CONTAINS 设置).click() # 包含“设置” s(predicatename ENDSWITH 量).click() # 结尾是“量” # 2. 数值比较 (常用于开关、滑块) s(predicatevalue 1).click() # 点击所有值为1的元素 s(predicatevalue 0.5).get() # 获取值大于0.5的元素 # 3. 布尔属性 s(predicateenabled true).click() # 点击所有已启用的元素 s(predicatevisible true AND enabled true).get() # 组合条件 # 4. 复杂逻辑组合 s(predicatetype XCUIElementTypeSwitch AND value 1).click() # 点击所有已打开的开关Class Chain 定位 Class Chain 语法类似于 XPath但专为 iOS 优化查询效率比 XPath 高。它特别适合描述元素的层级关系。# 1. 直接子元素 # 找到第一个类型为 Table 的元素下的第一个类型为 Cell 的子元素 s(classChain**/XCUIElementTypeTable/XCUIElementTypeCell[1]).click() # 2. 后代元素任意深度 # 找到任意位置的一个类型为 Button 且 name 为“完成”的元素 s(classChain**/XCUIElementTypeButton[name 完成]).click() # 3. 条件筛选 # 找到第二个类型为 Table 的元素下所有 name 以“通知”开头的 Cell cells s(classChain**/XCUIElementTypeTable[2]/**/XCUIElementTypeCell[name BEGINSWITH 通知]) for cell in cells.find_elements(): print(cell.name) # 4. 组合使用精确定位 # 在“设置”“通用”“关于本机”中定位版本号通常是一个StaticText # 假设层级为Window - NavigationBar - ... - Table - Cell[包含版本标签和版本号] version_cell s(classChain**/XCUIElementTypeTable/**/XCUIElementTypeCell[name 软件版本]) version_value version_cell.child(classChain**/XCUIElementTypeStaticText[2]) # 假设版本号是第二个StaticText print(软件版本:, version_value.text)如何选择简单场景用name,label,className组合。需要复杂属性匹配如值比较、模糊匹配用Predicate。需要描述精确的层级关系用Class Chain。尽量避免使用 XPath虽然 facebook-wda 也支持 (s(xpath‘...’))但在 iOS 上性能通常不如 Class Chain且对动态内容的支持可能不佳。4.3 元素操作点击、输入与滑动定位到元素后就可以与之交互了。# 点击操作 ele s(name蓝牙).get() ele.click() # 单击 ele.tap() # 与 click 等价 ele.tap_hold(2.0) # 长按2秒 ele.click_exists(timeout5.0) # 安全点击如果5秒内元素存在则点击返回True/False # 文本输入 search_field s(name搜索).get() search_field.set_text(自动亮度) # 输入文本 search_field.clear_text() # 清空文本 search_field.set_text(\b\b\b) # 模拟键盘删除键删除3个字符 search_field.set_text(NFC\n) # 输入文本并换行相当于点击键盘的“搜索”或“完成” # 滑动操作 # 基于像素坐标的滑动 c.swipe(200, 500, 200, 200, duration0.5) # 从(200,500)滑动到(200,200)耗时0.5秒 # 基于比例坐标的滑动更推荐适配不同分辨率 c.swipe(0.5, 0.8, 0.5, 0.2, duration0.5) # 从屏幕中部偏下滑动到中部偏上 # 便捷滑动方法基于当前窗口大小 c.swipe_up() # 向上滑动 c.swipe_down() # 向下滑动 c.swipe_left() # 向左滑动 c.swipe_right() # 向右滑动 # 获取元素信息 ele s(name蓝牙).get() print(f元素文本: {ele.text}) print(f元素名称: {ele.name}) print(f元素值: {ele.value}) print(f是否可用: {ele.enabled}) print(f是否可见: {ele.visible}) print(f位置和大小: {ele.bounds}) # 返回 Rect(x, y, width, height)注意事项set_text与clear_text对于文本输入框set_text会直接替换原有内容。如果想先清空再输入可以链式调用ele.clear_text().set_text(“new text”)。坐标系统iOS 的坐标原点(0,0)在屏幕左上角。使用比例坐标(0.5, 0.5)表示屏幕中心能更好地适配不同尺寸的设备。滑动速度duration参数控制滑动过程的持续时间。时间越短滑动越快。对于快速滚动列表可以设置较小的值如 0.1对于需要精确控制的拖拽操作可以设置较大的值如 1.0。5. 实战编写一个完整的自动化测试用例现在我们将所有知识点串联起来编写一个模拟用户操作的真实测试用例在“设置”中找到“NFC”选项并确保其处于关闭状态。这个用例涵盖了启动应用、搜索、元素定位、状态判断、条件操作、断言等关键步骤。#!/usr/bin/env python3 # -*- coding: utf-8 -*- import wda import time import pytest class TestSettingsNFC: 测试用例验证设置中的NFC开关可以正常关闭。 def setup_method(self): 每个测试方法开始前执行 # 初始化客户端使用USB直连需提前用tidevice启动WDA self.c wda.USBClient(00008101-000255021E08001E, port8100) # 等待设备就绪设置长超时避免环境问题 self.c.wait_ready(timeout300) # 设置全局隐式等待 self.c.implicitly_wait(30.0) print(测试初始化完成设备已连接。) def teardown_method(self): 每个测试方法结束后执行 # 终止设置应用清理环境 if self.c.session().app_state(com.apple.Preferences).get(value) 4: self.c.session().app_terminate(com.apple.Preferences) print(测试结束清理完成。) def test_disable_nfc(self): 测试关闭NFC功能 # 1. 启动“设置”应用 self.c.session(com.apple.Preferences) time.sleep(2) # 等待应用界面稳定 # 2. 下滑一点确保搜索框在视野中针对不同iOS版本适配 self.c.swipe_up() # 小幅度上滑让搜索框可能更靠下 time.sleep(0.5) # 3. 定位并点击搜索框输入“NFC” search_field self.c(name搜索).wait(timeout5.0) search_field.click() search_field.set_text(NFC\n) # 输入并执行搜索 time.sleep(1) # 等待搜索结果加载 # 4. 在搜索结果中点击“NFC”选项 # 使用Predicate定位更精确 nfc_cell self.c(predicatename NFC AND type XCUIElementTypeCell).wait(timeout3.0) nfc_cell.click() time.sleep(1) # 进入NFC设置页面 # 5. 定位NFC开关控件 # NFC开关通常是一个XCUIElementTypeSwitch且是当前页面唯一的Switch nfc_switch self.c(xpath//XCUIElementTypeSwitch).wait(timeout3.0) # 6. 获取开关当前状态并执行操作 switch_value nfc_switch.value print(fNFC开关当前状态值: {switch_value}) if switch_value 1: # 1 通常代表开启 print(NFC处于开启状态尝试关闭...) nfc_switch.click() # 点击开关进行切换 time.sleep(0.5) # 等待状态切换动画 # 处理可能出现的确认弹窗 (iOS 13) if self.c.alert.exists: print(检测到确认弹窗点击‘关闭’...) # 弹窗按钮文本可能是“关闭”、“Turn Off”等这里用包含‘关’字的匹配 for button in self.c.alert.buttons(): if 关 in button: self.c.alert.click(button) break time.sleep(0.5) # 再次获取开关状态 final_switch_value nfc_switch.value print(f操作后NFC开关状态值: {final_switch_value}) # 断言开关已关闭 assert final_switch_value 0, fNFC开关关闭失败最终状态为: {final_switch_value} else: print(NFC已处于关闭状态无需操作。) assert switch_value 0, fNFC开关状态异常值为: {switch_value} print(测试通过NFC功能状态符合预期。) if __name__ __main__: # 可以直接运行这个脚本 tester TestSettingsNFC() tester.setup_method() try: tester.test_disable_nfc() finally: tester.teardown_method()用例设计解析与技巧使用 Pytest 风格虽然这里用了if __name__直接运行但结构上采用了 Pytest 的setup_method和teardown_method模式便于集成到正式的测试框架中。健壮的等待策略wait_ready确保设备连接稳定。implicitly_wait设置全局隐式等待避免脚本因网络或设备响应慢而立即失败。关键步骤后使用time.sleep等待界面稳定或动画完成这是 UI 自动化中必不可少的“缓冲”。弹窗处理操作系统级开关如 NFC、蓝牙时iOS 经常会弹出二次确认框。脚本中通过c.alert.exists检查并处理弹窗提高了脚本的鲁棒性。状态断言不是简单地点击完就认为成功而是通过获取开关的value属性进行验证。‘1’通常表示开‘0’表示关但最好通过 Weditor 实际查看一下。定位策略混合使用展示了name、predicate和xpath的混合使用。在实际项目中应根据元素特点选择最稳定、最易读的定位方式。你可以将代码中的 UDID 替换成你自己设备的然后运行这个脚本。观察你的 iPhone “设置”应用是否被自动打开并完成了搜索 NFC 和检查开关的操作。6. 常见问题排查与进阶技巧即使按照指南操作你也可能会遇到一些坑。这里我总结了一些常见问题及其解决方法以及一些能提升脚本质量和效率的进阶技巧。6.1 常见问题速查表问题现象可能原因解决方案连接失败提示HTTPConnectionPool超时1. WDA 服务未启动。2. 端口被占用或防火墙阻止。3. 设备未连接或 UDID 错误。1. 检查tidevice wdaproxy命令是否成功运行。2. 尝试访问http://localhost:8100/status看是否有响应。3. 重新执行tidevice list确认设备 UDID。SessionNotCreatedError或无法启动应用1. Bundle ID 错误。2. 应用未安装在设备上。3. 开发者证书/权限问题。1. 确认 Bundle ID 是否正确。2. 检查设备上是否已安装该应用。3. 真机上需在“设置-通用-设备管理”中信任证书。元素找不到 (ElementNotFoundError)1. 定位表达式错误。2. 元素尚未加载出来。3. 页面层级发生变化如弹窗遮挡。1. 使用 Weditor 重新检查元素属性。2. 增加隐式/显式等待时间 (wait)。3. 检查是否有 Alert 弹窗先处理掉 (c.alert.dismiss())。操作无反应如点击无效1. 元素不可点击 (enabledfalse)。2. 坐标点击位置不对。3. 被其他元素遮挡。1. 检查元素enabled和visible属性。2. 尝试使用ele.click()代替坐标点击。3. 使用ele.bounds.mid_point获取元素中心点再点击。脚本在模拟器上正常真机失败1. 真机性能差异加载慢。2. 网络环境不同如使用Wi-Fi代理。3. 系统弹窗通知、权限申请干扰。1. 大幅增加等待时间。2. 确保电脑和手机在同一稳定网络或使用USB连接。3. 脚本开始时先处理常见弹窗或关闭通知。set_text不生效1. 目标元素不是文本输入框。2. 需要先点击输入框获取焦点。3. 输入法问题。1. 确认元素type是XCUIElementTypeTextField或XCUIElementTypeTextView。2. 在set_text前先执行ele.click()。3. 尝试在输入后发送\n或使用c.press(‘return’)。6.2 进阶技巧与最佳实践使用 Page Object 模式当测试用例增多时将页面元素定位和操作封装成单独的类使测试脚本更清晰更易于维护。class SettingsPage: def __init__(self, session): self.s session self.search_field lambda: self.s(name搜索) self.nfc_option lambda: self.s(predicatename NFC) def search_for(self, keyword): self.search_field().click() self.search_field().set_text(f{keyword}\n) def toggle_nfc(self): self.nfc_option().click() # 在测试用例中 page SettingsPage(c.session(com.apple.Preferences)) page.search_for(NFC) page.toggle_nfc()截图与日志记录在关键步骤或断言失败时截图便于后期调试。def take_screenshot(self, filename): 截图并保存 import datetime timestamp datetime.datetime.now().strftime(%Y%m%d_%H%M%S) path f./screenshots/{filename}_{timestamp}.png self.c.screenshot().save(path) print(f截图已保存: {path}) return path # 在测试步骤中调用 self.take_screenshot(before_click_nfc)处理系统弹窗和权限在测试开始前可以尝试预设一些权限或处理已知弹窗。# 示例处理“允许通知”弹窗 with c.alert.watch_and_click([允许, 好, OK]): # 在这个代码块内如果出现弹窗且按钮文本匹配会自动点击 launch_my_app()优化执行速度减少不必要的等待用wait代替固定的sleep。批量操作对于连续的同类型操作可以考虑减少中间的状态检查。复用 Session避免在同一个测试中反复启动和关闭应用。集成到 CI/CD结合 Pytest、Allure 生成美观的测试报告并通过 Jenkins 或 GitLab CI 在代码提交后自动执行 iOS 自动化测试套件。核心是确保 CI 机器上也能通过tidevice连接和管理 iOS 设备通常需要将设备连接到 CI 服务器。通过掌握这些排查方法和进阶技巧你的 facebook-wda 自动化测试脚本将变得更加稳定、高效和可维护。记住UI 自动化测试的本质是模拟用户操作因此多从用户视角思考并让你的脚本像用户一样“耐心”和“聪明”地处理各种界面状态是成功的关键。