因为公司一些自动化测试场景需要(对官方 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 的测试工具,有其独到之处:

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

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

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


↙↙↙阅读原文可查看相关链接,并与作者交流