导语
最近很多团队都在接入移动端的UI自动化,相信大家在使用过程中一个很大的困惑就是如何定位页面上的一个控件,从最大化降低自动化的维护成本上考虑,我们希望这个控件id是唯一的,相对稳定的,对于安卓app来讲,很多控件是带有resource-id的,这个是因为开发同学在使用过程中同样需要这个id去定位元素,但是对于iOS应用,这个问题是比较头疼的,因为不管是Macaca底层所依赖的XCUITest,还是Appium所依赖的UIAutomation,都是通过accessibilityIdentifier来定位元素,但是在iOS开发中,基本没有开发同学会去设置这个id,因此造成一个现状是对于iOS应用的元素定位,大家需要变换多种策略,比如通过class,name,xpath等等其他属性来获取,如果能给iOS上的控件设置id,将能大大的方便UI自动化的工作,更进一步,如果这个添加的过程能自动化实现,而不需要开发同学手工添加,就更加事半功倍了。这篇文章便是对这一策略的实践。
技术可行性分析
了解iOS开发的同学都知道,所有视图都是UIView或者其子类,如果我们能拦截到所有UIView初始化的入口,添加生成id的处理,就能为所有的UIView添加id了,这点是可行的,通过hook UIView的load方法就可以实现。
怎么生成唯一的id?
现在我们已经确定是可以通过Hook的方式添加id的,但是这个id要如何取值,这又是一个问题,上面导语部分已经讲到,要最大化的降低自动化脚本的维护成本,我们希望这个id在单页面内是唯一的,同时又是相对稳定的,不会因为一次版本的升级就变化,这是在思考中最纠结的一个点,因为要找到一种策略满足这个要求真的不是一件容易的事情,后来在网上看到一篇文章,深受启发,详情参考:http://yulingtianxia.com/blog/2016/03/28/Add-UITest-Label-for-UIAutomation/
概括一下其主要思路如下:
- 如果控件对应变量是某个类的属性,则取其属性名称作为标签id。因为代码没有改动,则标签也不会变化。即使代码有变动,也肯定是因为业务逻辑变更导致了界面上的变化,那么测试脚本肯定也是要改的,所以无需多虑此种情况。
- 如果控件对象是临时创建的局部变量,同一页面中很有可能有相同名字的局部变量。而且 Objective-C Runtime 无法获取局部变量名称,所以针对此种情况尽量采用其他来源的内容作为标签。比如对于UILabel(文本控件),可以取其text作为id,对于UIButton(按钮)对象,可以取其title标题作为id,对于UIImage对象,可以取其资源名也就是image作为id.
实践
实践是检验真理的唯一标准。我们按照对应的思路在我们的应用工程中添加了对应的处理,主要体现为新增两个扩展类,如图所示:
下面我们对比一下在进行Hook操作前后对同一个视图的inspector的结果:
在处理之前,我们对某app进行inspect,得到视图树如下:
在我们添加了对应的几个文件之后,重新编译,然后对app进行Inspect,得到视图树如下:
可以看到视图中的控件都已经添加了对应的id,对应dom树中的rawIdentifier控件,就是我们需要的标签了,这样我们就可以通过这个id来操作这个金额控件。在此之前,我们想点击这个金额控件是不太好办的,因为这个控件没有id,他的class是StaticText,而这个类在整个视图中有若干个,通过class去取很难,xpath一是太长,二是一旦视图层级变化,xpath就会变化,维护成本会很高,也不可取,现在我们就可以通过操作这个id直接拿到这个控件了,而且这个控件取的是代码里对应这个控件的属性值,除非逻辑发生变化,一般没有人去修改一个变量的名字,维护成本也相对降低了,可见这个方案是可取的。
完美了吗?
通过上面的方案,我们实现了给控件自动生成id,但是这个方案能完美解决我们的需要吗?
答案是否定的,因为你会发现,视图中有可能存在两个控件,他们的Id是一样的!就像下面这个:
登录页获取“手机号”控件id:
登录页获取"密码"控件id:
我们会发现对于“手机号”和"密码"两个控件,他们的id是一样的,这是为什么呢?
这就要回到我们上面讲到的生成id的方案,我们讲到,对于一个控件,如果他是对应父类的一个属性,那我们会取他的属性名字作为控件的id,到这里相信大部分同学已经找到了上面问题的答案:因为手机号这一行和密码这一行,他们是同一个UIView的子类,“手机号”和“密码”这两个控件,他们都是同一个属性!
问题已经定位,接下来要怎么做呢?要保证控件的唯一性吗?
如果我们一定要保证每个控件的唯一性,也不是没有办法,最简单的就是我们给同一个属性的控件添加序号标记,根据出现的先后,依次为控件添加编号,就能实现控件唯一的效果。
但是,一定要这么做吗?
这时候,我们要回归到我们的应用场景上来,什么时候会出现这种id重复的情况呢,在一个视图中存在同一个UIView的多个对象,这种场景,更多的是出现在我们的列表视图中,也就是iOS的tableView中,对于这样的视图,我们自动化的策略是什么呢?一般情况下,这种视图的数据都是服务端返回的,而我们在对这样的视图进行自动化的时候,无非是下面几种策略:
- 遍历点击所有的cell
- 选择某一个特定的cell进行操作,比如第3行,也就是Index = 2
在这种情况下,一个唯一的id对我们的作用有多大呢,事实上在这种场景下,我们需要的是一个数组,对于同类控件的一个数组,这种场景下同样的id对我们反而是比较好用的,因为我们可以拿到这个数组,然后决定做遍历操作还是对某一个索引的控件做指定的操作。
因此,我们的最终策略是保持这种方式,没有最完美的,只有最适合的,that's all!
风险?
这种方案通过Hook底层的UIView来实现id的自动添加,如果项目源码本身没有对UIView做同样的hook操作是没有什么风险的,如果对这一点有顾虑,可以考虑将对应的文件只加在debug包中,对于release包,不将对应文件加入编译,这样我们可以在debug包中进行自动化测试,而对于线上包则没有任何影响。
源文件
https://github.com/Yinxl/iosHookViewId
使用方法:将hook目录下的几个文件拖进XCode工程下面编译即可
附录
“Macaca开源社区”群的钉钉群号: 11775486(欢迎入群讨论)