自动化工具 WebDriverAgent 踩坑记

陈恒捷 · 2016年06月29日 · 最后由 小断腿 回复于 2020年05月12日 · 5776 次阅读
本帖已被设为精华帖!

因为公司一些自动化测试场景需要(对官方 app 进行 UI 自动化、多机并行跑 monkey ),这几天在基于 appium python client 的基础上编写 WebDriverAgent 的 python client,对 WebDriverAgent 进行单独支持。目前已经完成得差不多了,过程中发现不少坑,在这里记录下,顺便吐下槽。

PS:这个 client 由于公司政策不能放出来,请见谅。

关于和 WebDriver Spec 不一样的部分

这部分最坑。本来实现的是 WebDriver Spec 已经定义好的功能,结果实现方式还不一样,导致需要自己覆盖原有的 client 方法。

注:打字太累,下面 WebDriverAgent 均简称 WDA

1、 获取页面元素树 (page_source)

原请求:GET /source
WDA 实现:POST /source

另外要注意的是,WDA 返回的元素树默认是 json 格式,client 在进行 json 转 dict 时会把元素数也转为 dict ,需要手动转化回 json string 。

2、 错误信息处理

原格式:{'status': 1, 'sessionId': xxx, 'message': 'error message'}
WDA 实现:{'status': 1, 'sessionId': xxx, 'value': 'error message'}

如果是找元素出错,还会变成 {'status': 1, 'sessionId': xxx, 'value': {'description': 'error message', 'using': 'class name', 'value': 'test'}}

要修改的话,直接在 error_handler 修改 Exception 中的处理就好。

3、获取 textfield 元素的 text 属性

当输入框没有内容时,返回的 value 是 null(在 python client 里面会自动转换为 None )。记住:不要通过空字符串来判断 textfield 是否为空哦。当然,你也可以 override text 方法,把 None 转换回空字符串。

WebDriverAgent 自带的坑

1、 查找元素

使用 inspector 时,属性名显示为 Class 。使用 source 时,属性名变成了 type 。当然搜索时,用 find_element_by_class_name('testClass') 等价于 find_element_by_link_text('type=testClass')

还有另一坑,在 inspector 和 source 中显示的其实都是精简后的名称。例如 inspector 中显示 'Class: Button' ,但你用 'Button' 这个 class name 去找是死活找不到的,因为在查找元素的时候它的完整名称是 XCUIElementTypeButton 。也就是你必须用 XCUIElementTypeButton 这个名称作为 class name 才能找到这个元素。

用 xpath 的同学也要注意,json 中的元素属性值和 xpath 会有些出入。举例如下:

{
          "name": "ComputeSumButton",
          "isEnabled": "1",
          "value": null,
          "label": "Compute Sum",
          "isVisible": "1",
          "rawIdentifier": "ComputeSumButton",
          "type": "Button",
          "frame": "{{94, 122}, {113, 37}}",
          "rect": {
            "origin": {
              "y": 122,
              "x": 94
            },
            "size": {
              "width": 113,
              "height": 37
            }
}
<XCUIElementTypeButton type="XCUIElementTypeButton" name="ComputeSumButton" label="Compute Sum" private_indexPath="top,0,0,2" />

其中 xml 文件需要自己手动在 FBFindElementCommands.m+ (NSArray<XCElementSnapshot *> *)descendantsOfElementSnapshot:(XCElementSnapshot *)elementSnapshot withXPathQuery:(NSString *)xpathQuery 自己打断点:

+ (NSArray<XCElementSnapshot *> *)descendantsOfElementSnapshot:(XCElementSnapshot *)elementSnapshot withXPathQuery:(NSString *)xpathQuery
{
  NSMutableDictionary *elementStore = [NSMutableDictionary dictionary];
  DDXMLElement *xmlElement = [self XMLElementFromElement:elementSnapshot indexPath:@"top" elementStore:elementStore];
  NSError *error;
  // Add break point here!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  NSArray *xpathNodes = [xmlElement nodesForXPath:xpathQuery error:&error];
  if (![xpathNodes count]) {
    return nil;
  }

  NSMutableArray *matchingSnapshots = [NSMutableArray array];
  for (DDXMLElement *childXMLElement in xpathNodes) {
    XCElementSnapshot *element = [elementStore objectForKey:[[childXMLElement attributeForName:kXMLIndexPathKey] stringValue]];
    if (element) {
      [matchingSnapshots addObject:element];
    }
  }
  return matchingSnapshots;
}

打完断点后在 lldb 的调试窗口里输入 po [xmlElement xmlString] 获取。xpath 的查找也是基于这个 xml 的。

2、查看元素属性值

留意官方的 integration test FBElementAttributeTests.m,不是所有元素都有 name, label 这两个属性值。如果刚好没有需要的属性值,轻则返回 None,重则直接在 WebDriverAgent 端产生错误。

关于如何从源码获取接口 api 相关信息

虽然官方给了一个 wiki,但里面给到的 api 并不全。不过好在 WebDriverAgent 的代码结构还是比较清晰的,所以也可以简单地通过源码获取 api 信息。

接口相关处理文件全部在 WebDriverAgentLib/Commands 中。

route

一般一开始就是此文件处理的所有 command 的 route 列表。例如 FBDebugCommands.m 中:

+ (NSArray *)routes
{
  return
  @[
    [[FBRoute POST:@"/source"] respondWithTarget:self action:@selector(handleGetTreeCommand:)],
    [[FBRoute POST:@"/source"].withoutSession respondWithTarget:self action:@selector(handleGetTreeCommand:)],
  ];
}

其中第二个请求带有 .withoutSession ,表明请求中可以不带有 sessionId 这个字段。这个 route 应该是给 inspector 用的。

arguments

通过查看 route 指定的 selector 可以找到对应 handler ,handler 中如果会获取请求中的参数,会以 request.arguments[@"argName"] 的形式获取,如 FBDebugCommands.m

+ (id<FBResponsePayload>)handleGetTreeCommand:(FBRouteRequest *)request
{
  FBApplication *application = request.session.application ?: [FBApplication fb_activeApplication];
  if (!application) {
    return FBResponseWithErrorFormat(@"There is no active application");
  }
  const BOOL accessibleTreeType = [request.arguments[@"accessible"] boolValue];
  return FBResponseWithStatus(FBCommandStatusNoError, @{ @"tree": (accessibleTreeType ? application.fb_accessibilityTree : application.fb_tree) });
}

可以看到它会处理请求中的 accessible 参数,并会先转化为 boolean 类型。因此请求中可以带有 accessible 参数,且它的值必须是 true 或者 false

总结

WebDriverAgent 作为新的基于 XCUITest 的测试工具,有其独到之处:

  • 支持一台 mac 连接多台设备进行测试(走 xcodebuild ,不走 instruments)
  • 任意应用的 UI 测试(悄悄告诉你,苹果的桌面也是一个应用,它的 bundle id 是 com.apple.springboard 哦)
  • 支持跨应用(任意界面都能控制)

而且稳定性也还可以接受(针对所有支持命令编写的 50 条测试用例基本可以稳定保持通过,只是偶尔响应速度比较慢)。

虽然目前还有前面提到的这些坑,但都不是什么大坑,只要少许修改就能填掉。相信经过逐步完善,会像现在 Android 的 UIAutomator 那样成为主要的 UI 自动化测试框架之一。

最后,如果有同学想练手同时熟悉 appium python client ,不妨自己动手做个 WebDriverAgent python client 出来哦。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 82 条回复 时间 点赞

@chenhengjie123 我通常比较喜欢直接把 API 背下来的。因为一个查找元素的问题一直卡着,不过终于写完了,测试了下,已经可以用了 https://github.com/codeskyblue/python-wda 纯 Python 的封装

陈恒捷 回复

我这边也遇到了这个问题,我这边直接 app 都卡死了,请问一下是怎么解决的呀,

你好,我在真机上运行多条用例,每次运行十几条用例之后就会出现连接超时,看 xcode 里面显示获取不到真机活动列表,以前完全没接触过 ios 测试,大佬帮忙看看呢,是哪里的问题呀,怎么解决呀
用例报错信息:
requests.exceptions.ReadTimeout: HTTPConnectionPool(host='localhost', port=8100): Read timed out. (read timeout=60.0)
xcode 信息:
t = 7350.54s App event loop idle notification not received, will attempt to continue.
t = 7350.54s App animations complete notification not received, will attempt to continue.
2020-04-16 17:37:03.908945+0800 WebDriverAgentRunner-Runner[6400:1970311] Enqueue Failure: Failed to get system application: Timed out while fetching attributes 'XC_kAXXCAttributeSystemAppApplication' for Device system-wide element. WebDriverAgentRunner/UITestingUITests.m 38 1
2020-04-16 17:38:08.913791+0800 WebDriverAgentRunner-Runner[6400:1970311] Enqueue Failure: Failed to get list of active applications: Timed out while fetching attributes 'XC_kAXXCAttributeFocusedApplications' for (null). WebDriverAgentRunner/UITestingUITests.m 38 1

#69 楼 @jira 嗯,应该是这个。

Monkey 将本帖设为了精华贴 06月29日 16:49
匿名 #90 · 2016年06月29日

赞,支持一下

我已经实现了 WDA 的 python client,不基于 appnium 的,走的 USB 通信。我有个疑问,难道你没有遇到长时间运行 WDA 内存泄漏最终导致低内存 crash 么?

还有 getPageSource, 直接发挥的是 Json 格式. 我也是醉了, 我也得做适配.

#3 楼 @gogle 我也没基于 appium ,自己另外实现的,走的是 Selenium 的 remote server,通过 ip 通讯。

因为我目前阶段还只是适配 client ,还没有在项目中长时间运行 WDA ,内存泄露还不明显。明天搞个循环执行我那 50 条 api 测试用例 100 次,看看内存情况如何。

不过官方 issue 里面有找到这个问题,应该是就是你提的吧?

#4 楼 @seveniruby 是啊,真心不明白它为啥要搞成 json 。。。

#5 楼 @chenhengjie123 哈是我提的,不过人家不鸟我。我用来跑 monkey,iPhone6 上可以跑 1 个小时,然后 XCTRunner 就会 crash 了。

#7 楼 @gogle 我跑 monkey 用的是 tap 随机坐标和 dragFromToWithDuration 滑动随机位置,没有获取元素树。不过我也没观察过具体能跑多久,明天再看下。

#4 楼 @seveniruby
#5 楼 @chenhengjie123 有个巨大的坑,元素多了,就费了。 需要改造。

#9 楼 @lihuazhang 具体达到多少元素的时候就会废了?

#9 楼 @lihuazhang 是的,实测控件树有 236 个元素,耗时 2.6 秒,这仅仅是获取控件树的耗时。

好消息,WebDriverAgent 终于准备修改它不符合 WebDriver Spec 的部分了:https://github.com/facebook/WebDriverAgent/issues/179

区曼 [该话题已被删除] 中提及了此贴 07月05日 22:48

WDA 怎么才能 tap 一个坐标呢,而不是 tap 一个元素

#14 楼 @codeskyblue 请求大概长这样:

curl -X POST $JSON_HEADER \
-d "{\"x\":\"10\",\"y\":\"20\"}" \
$DEVICE_URL/session/$SESSION_ID/tap/5

末尾的 5 是 element id 。如果根据 element id 能找到元素,那么 x, y 会被当做是元素左上角坐标的偏移量。如果找不到(例如给个-1),那么会当做屏幕的坐标。

详细源码在WebDriverAgent/WebDriverAgentLib/Commands/FBElementCommands.m 里面。官方的 Queries 文档只列出了所有命令中的 70% 左右。

有空的时候我试下用 Jmeter 录制下我的 python-client 测试过程中所有请求给你吧,我实现了大约 85% 的命令。

#14 楼 @codeskyblue 录好了,一个 jmeter 的,一个 charles 的。都放网盘了:http://pan.baidu.com/s/1slEdvBn

#15 楼 @chenhengjie123 thank you very much, it works.

请教个问题,跑起来 WebDriverAgent 必须在 Mac 上吗? 还必须用 xcodebuild 吗?还有那个 USB Support 该怎么用?

#16 楼 @chenhengjie123 你这两个都是啥,怎么用

#19 楼 @codeskyblue

  1. 必须在 Mac 。所有支持的启动方式在 facebook 的 wiki 里有写。
  2. USB Support 我也没用过,不大清楚。 @lihuazhang 有用过不?
  3. 网盘里一个是 jmeter 的测试脚本,不过是直接录制的,直接跑肯定报错(session id 不对)。另一个是 charles 的抓包记录。你可以通过看里面的内容了解一下一些官方没有给到的命令怎么使用。因为最近比较忙,还没整理,都是直接录制的。。。如果有看不懂的可以再问下我。

其实它的命令还是直接看源码比较快,看我录制的请求可能还不大清楚具体是干嘛的。这两天有空的话我把录制出来的请求名称改成对应功能。

@gogle USB 通信你是怎么做的,方便说下吗?

#21 楼 @codeskyblue 参考 peertalk 这个项目

https://github.com/codeskyblue/python-wda 点击 Element,以及键盘输入,还不知道具体的步骤,求助下

#24 楼 @codeskyblue 看了下你的代码,你是从零开始写啊。我比较建议你仿照 appium python client 的形式来写,少写很多东西,大家用起来也习惯,可维护性也更好。

键盘输入和点击 element 和 selenium 的保持一致。

@chenhengjie123 也不算是从零开始,参考了下 https://github.com/xiaocong/uiautomator 这种方式比较 pythonoic

#26 楼 @codeskyblue 哈哈,确实也是。不过从通用性角度,用回和 appium python client 一致的形式会比较方便咯~

#27 楼 @chenhengjie123 只是另一种形式而已,不打算改成 appium 的格式,我无意去喷 appium 的形式,函数名太长,而我平常又是一位不喜欢用自动补全的人,那么长的函数名,简直是要死人了。

#28 楼 @codeskyblue 额,函数名这个它也只是跟着 selenium 的规范走。

函数名长确实不好用,但好处是遵循这个规范的话一些基于 selenium api 的工具可以直接拿来用,例如 python page object 。而且也能保证大家都能看懂。有时候,大家能看懂比输入效率重要。

函数名长度这个我在看了苹果的函数名后就没啥感觉了。。。都长得要死。。。

工程大了我反而喜欢命名长的了...【前提是命名规范清晰

#29 楼 @chenhengjie123 可是平常使用的话,也都是边翻文档,边写 case 呀,除了 PO,还有什么东西,不遵守这个命名规范,所遗失的。
另外,我也同意看得懂比效率更重要,毕竟看得懂很重要,而且以后维护还需要经常用呢,可函数名长了未必就好懂呀

#31 楼 @codeskyblue 我觉得主要是学习成本。毕竟自己写的不一定经过足够的测试检验,存在的 bug 会比较多,也不一定能给出足够丰富的文档(直接看源码这种不算文档哈)。

其实也没所谓啦,大家都能看得懂,效率也高,那就最好啦。用 IDE 自动补全的话,长命名有时候反而效率高呢。

#33 楼 @codeskyblue 赞~补充一下文档吧,可以的话补充一些测试代码,同时也可以作为 sample code 。

恒温 关闭了讨论 07月12日 21:09
恒温 重新开启了讨论 07月12日 21:09

#34 楼 @chenhengjie123 readme 里基本都写了,例子在 tests 目录下

#37 楼 @codeskyblue 只有这么少 api 么?

#38 楼 @chenhengjie123 还有 swipe,tapAndHold 等一些拖动 api 还没加上,不过现在这些功能,感觉跑个应用应该是足够了

#11 楼 @gogle
#22 楼 @lihuazhang 你们是通过 instruments 的内存相关模板还是直接 xcode 的监控来看内存泄露的?

我在运行 WebDriverAgent 时在 xcode 监控界面选择 profile in instruments ,xcode 会自己挂掉,然后 WebDriverAgent 也会被停掉。

#21 楼 @codeskyblue usb 通信并不难,按照 protocol 发送 socket 即可。USB 通信不要求手机有网络,而且更稳定。

#40 楼 @chenhengjie123 就直接在 xcode 上的看得。当然,我也成功使用了 instrument 的对应模板查看。方法是用 xcodebuild 启动,然后 attach 到手机的 xctrunner 进程

#42 楼 @gogle 是命令行启动 WDA,然后 GUI 的 instruments attach 到 xcrunner 进程吗?

我一 attach 它就停了,获取不到数据。。。Xcode 7.3.1 + iOS 9.3 。我回家用家里电脑自己再试试吧。谢谢。

#41 楼 @gogle 多谢,我有空试试

#41 楼 @gogle 我发现直接用 iproxy 直接就把手机里的端口转发到电脑上了,似乎更简单

#45 楼 @codeskyblue 是的,我一开始也直接用了端口映射 (tcprelay 也可达到类似目的)。但这样还是依赖手机的网络

#46 楼 @gogle 嗯,了解了。不过我还是不清楚该怎么下手,有这样的 python 库可以用的吗?

连接多台模拟器的这个, 不知道你那边尝试过吗? 我使用 fbsimctl 利用 webdriveragent 接口打开程序时会失败

#48 楼 @noshuai 我解决这个问题了, 命令少了一个空格.

#47 楼 @codeskyblue 各种方式联系你,联系不到你啊,可否加一下 qq 呢?872489864,给你还发了邮件。。。。。

#50 楼 @diao2007 邮件你往哪里发的,是 gmail 吗,根本没收到呀

#50 楼 @diao2007 gitter.im 上也能找到我呀

不二家 [该话题已被删除] 中提及了此贴 07月26日 17:01

请问下各位大神
curl -X POST '-H "Content-Type: application/json"' -d "{\"using\":\"partial link text\",\"value\":\"label=Alerts\"}" http://10.2.211.186:8100/session/$SESSION_ID/element
这定位出来的 element 每次 id 都是变化的。怎么破。

#14 楼 @codeskyblue 看到你也这么问过,看来是没有简单的坐标点击方法 tap(x, y) 吧?

#58 楼 @chenhengjie123 没有,都知道这个文件有什么用。

build 的时候遇到过报错问题,谁有没有遇到过或解决方法

#57 楼 @luobs 看起来是 js 语法错误导致 webpack 打包失败。你有改动过这个 js 文件吗?

#60 楼 @jira 当时做得很简单,就是两个随机事件:click 和 swipe,然后每个事件的具体坐标随机生成。

#62 楼 @jira 有的,处理方式参考的是 CrashMonkey4iOS ,用一个 idevicedebug 命令让指定应用恢复前台。我当时为了简单,每隔 10 个事件就调用一次这个命令。

#61 楼 @chenhengjie123 那如果跳到其它 App ,这个有处理吗

可不可以分享一下 monkey 测试的思路?

#63 楼 @chenhengjie123 好方法啊,https://testerhome.com/topics/3766,我看你这篇帖子没有讲到具体怎么去恢复前台,能补充一下不?

#64 楼 @jira 你查下 crashmonkey4ios 源码?就是一个命令,具体是啥命令我也忘了

#66 楼 @jira 它源码是 ruby 写得,不过那句命令是外部调用的,有开发经验应该能很快找到。命令名称我之前发过给你了。

#67 楼 @chenhengjie123 idevicedebug -u UDID run BUNDLEID

区曼 Ant+JMeter+WebDriverAgent 游记 中提及了此贴 12月22日 14:08

#70 楼 @chenhengjie123 你有做导出 crash 报告吗

#72 楼 @jira 没,我们的应用接入了 crash 收集平台,直接看设备的 crash 日志也可以满足需要。

#57 楼 @luobs 我也有这个问题 请问你解决了吗

#74 楼 @yefnegjun 需要独立下载 WDA 执行,然后覆盖到安装路径下

不用 n 个目录吧,一个 webdriveragent 目录运行多个 xcodebuild 就好了

陈恒捷 回复

嗯,可以,之前不知道哪里冲突,一直运行报错...

您好啊,我在往真机上 run webdriveragent 的时候,报这个错呢,求指导😩

*** If you believe this error represents a bug, please attach the result bundle at /Users/wanghuan24/Library/Developer/Xcode/DerivedData/WebDriverAgent-akfdosudpqimrfewltpeoxnchjhe/Logs/Test/Test-WebDriverAgentRunner-2019.01.22_15-22-11-+0800.xcresult

2019-01-22 15:22:27.691 xcodebuild[1721:47415] [MT] IDETestOperationsObserverDebug: 14.732 elapsed -- Testing started completed.
2019-01-22 15:22:27.691 xcodebuild[1721:47415] [MT] IDETestOperationsObserverDebug: 0.000 sec, +0.000 sec -- start
2019-01-22 15:22:27.691 xcodebuild[1721:47415] [MT] IDETestOperationsObserverDebug: 14.733 sec, +14.732 sec -- end
2019-01-22 15:22:27.691 xcodebuild[1721:47415] Error Domain=com.apple.platform.iphoneos Code=-12 "Unable to launch com.apple.test.WebDriverAgentRunner-Runner" UserInfo={NSLocalizedDescription=Unable to launch com.apple.test.WebDriverAgentRunner-Runner, NSUnderlyingError=0x7fa9d93b4fd0 {Error Domain=DTXMessage Code=2 "Failed to deserialize message" UserInfo={NSLocalizedDescription=Failed to deserialize message, NSUnderlyingError=0x7fa9d99c4b00 {Error Domain=NSCocoaErrorDomain Code=4864 "value for key 'NS.objects' was of unexpected class 'NSException'. Allowed classes are '{(
NSURL,
NSData,
NSDictionary,
NSArray,
NSString,
NSAttributedString,
NSNumber,
NSValue,
NSDate,
NSSet,
NSError,
NSNull
)}'." UserInfo={NSDebugDescription=value for key 'NS.objects' was of unexpected class 'NSException'. Allowed classes are '{(
NSURL,
NSData,
NSDictionary,
NSArray,
NSString,
NSAttributedString,
NSNumber,
NSValue,
NSDate,
NSSet,
NSError,
NSNull
)}'.}}}}}
2019-01-22 15:22:27.692 xcodebuild[1721:47415] Error Domain=IDETestOperationsObserverErrorDomain Code=4 "Failed to install or launch the test runner" UserInfo={NSLocalizedRecoverySuggestion=If you believe this error represents a bug, please attach the result bundle at /Users/wanghuan24/Library/Developer/Xcode/DerivedData/WebDriverAgent-akfdosudpqimrfewltpeoxnchjhe/Logs/Test/Test-WebDriverAgentRunner-2019.01.22_15-22-11-+0800.xcresult, NSLocalizedDescription=Failed to install or launch the test runner, NSUnderlyingError=0x7fa9d96c5960 {Error Domain=com.apple.platform.iphoneos Code=-12 "Unable to launch com.apple.test.WebDriverAgentRunner-Runner" UserInfo={NSLocalizedDescription=Unable to launch com.apple.test.WebDriverAgentRunner-Runner, NSUnderlyingError=0x7fa9d93b4fd0 {Error Domain=DTXMessage Code=2 "Failed to deserialize message" UserInfo={NSLocalizedDescription=Failed to deserialize message, NSUnderlyingError=0x7fa9d99c4b00 {Error Domain=NSCocoaErrorDomain Code=4864 "value for key 'NS.objects' was of unexpected class 'NSException'. Allowed classes are '{(
NSURL,
NSData,
NSDictionary,
NSArray,
NSString,
NSAttributedString,
NSNumber,
NSValue,
NSDate,
NSSet,
NSError,
NSNull
)}'." UserInfo=0x7fa9d99d0380 (not displayed)}}}}}}

Testing failed:
WebDriverAgentRunner-Runner.app encountered an error (Failed to install or launch the test runner. (Underlying error: Unable to launch com.apple.test.WebDriverAgentRunner-Runner. (Underlying error: Failed to deserialize message. (Underlying error: The data couldn’t be read because it isn’t in the correct format. The data isn’t in the correct format.))))

jmei 回复

获取网络 timeout 异常?

陈恒捷 回复

运行多个用例的过程中 wda 断开连接了,有办法感知到么

不二家 WebDriverAgent 的 python client 端使用记录 中提及了此贴 04月24日 17:14

你好,请问 wda 启动的时候必须要链接外网才可以吗?没有外网启动不了啊

16楼 已删除

WDA build 时,即便用了企业证书,也会偶发性出现 65 错误。以及一些稀奇古怪的错误,稳定性还是有待考量。

WDA build 时,即便用了企业证书,也会偶发性出现 65 错误。
以及一些稀奇古怪的错误,稳定性还是有待考量。

jmei 回复

报错信息发上来看下?

建议你另外开贴,记录下你的详细步骤和关键代码吧?只有一个日志不好确定是哪里有问题。

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册