【Android测试】UI自动化代码优化之路(临时发布, 随时删除)

时间:2021-08-25 11:42:35

版权声明:本文出自胖喵~的博客,转载必须注明出处。

  转载请注明出处:http://www.cnblogs.com/by-dream/p/5993622.html

关于UI自动化的抱怨


  听过不少人这样讲 “UI自动化非常不稳定,需求一改,界面一遍,全部都费了”。我相信做过的人可能也会有同感。既然这个问题一直都是存在的,那么为什么没有人仔细分析原因呢?

  我的老板george曾举了这样一个例子:每当需求变化的时候,开发没有跳起来,反而是测试跳了起来。然后不断的抱怨,界面元素全都改了,我的自动化的用例全部都要废弃掉了。那么我们是否想过,为什么开发可以从容不破的应对产品不断变化的需求?而我们却不能呢?

  【Android测试】UI自动化代码优化之路(临时发布, 随时删除)

  业内不少人也都放弃了UI自动化,觉得接口测试才是最有意义的,的确接口测试相对于UI自动化的来说,确实稳定的多,但是难道我们测完接口后,就不需要再测试UI了吗?UI层可是最贴近用户,也就是直接给用户最直接的一层,我们真的要放弃吗?换句话说,如果客户端开发在分层做的特别差的时候,客户端的接口测试真的会那么好做吗?按下一个home键后再回来,onStop、onResum等一些生命周期里的函数恐怕都很难应付的过来吧。说了这么多,还是回到正题吧,讲一讲如何写好我们的UI自动化测试工程。(注意,这里是工程,不是脚本,脚本是很随意的,随便写两句用来测试的,工程就代表是一个可以交付给用户使用的,我们可以认为开发的代码是我们要交付给用户的产品,当然我们的测试代码同样也是要交付给用户的产品,所以后面统一叫测试工程)

简陋的测试代码


  首先来看看之前我们的UI自动化的代码是怎么写的:

  【Android测试】UI自动化代码优化之路(临时发布, 随时删除)

  这里我使用了Espresso+Uiautomator2框架来进行举例,忽略语法,只说结构的话是不是看上去很熟悉,大家在起初写测试代码的时候应该也是这样的一个方式吧:在测试函数中完成所有的操作实现,如果是通用的操作,可能去封装一个函数,当然有的时候还会用到setup进行初始化,使用teardown来进行收尾。我想说如果是这样的写法的话,我们的确很难应对变化的需求和界面了,恐怕测试框架的更新换代,都能让我们的测试代码全部推翻重写。其实综合起来我们之前的代码存在这样的一些问题:

  1、用例层包含了太多和源码相关的内容,只要源码改动,用例内容必改。

  2、用例层直接调用原框架内容进行实现,无法处理一些通用的异常情况,也不能适应工具框架的变化。

  3、通用的一些方法(例如登录),多个用例集(**Test类)中的用例(test**()函数)可能都需要复用,需要处理好调用的方式。

  4、启动Activity这样的操作不必要每个用例集都写,可以优化一下,抽象出基类。

  因此针对这些地方,我们需要进行优化,我们接着往下看。

关键字驱动


  yoyo(GT的开发)建议我使用这样的一套用例编写的方案,就是基于关键字驱动的方案,在用例层的实现剥离和源代码相关的部分,使用高度抽象。这样的话只要业务的流程不变,界面任意怎么修改,我们都不需要去修改用例层的代码。整个框架的实现结构如下:

  【Android测试】UI自动化代码优化之路(临时发布, 随时删除)

  这里如果大家看类图有些吃力的话,可以先看看UML类图介绍。这里先大致说下这些类模块的作用:

  BaseTest:所有Test类的基类,也就是测试用例的基类,里面实现ActivityTestRule来启动Activity,如果有需要的情况下可以实现BeforeClass和AfterClass,这两个在整个命令的运行周期内只在开始和结束的地方执行一次。

  用例Test:具体的测试用例的实现类,这个可以理解为一个测试集,每个类中有若干test函数,每个函数就代表一个测试用例,用例的写法采用关键字驱动的方法。

  Key:用枚举定义着所有的关键字。

  Command:接口类,供Word实现execute(Obj)的方法。

  FrameCommand:基类,供Word层来继承,里面只封装了一个execute(Key, Obj...),主要用在AW中调用KW的实现;这里需要注意和Command接口中execute的区别。

  Word层:即图中Login、Enter**Page等,需要实现Command接口中的execute函数,同时继承自FrameCommand,解释为什么是Word层,这里需要把这些实现抽象成ActionWord(简称AW)、KeyWord(简称KW),而这两者的区别就是,KeyWord中实现可复用的一些场景,ActionWord中可以包含KeyWord,实现一些很少被复用的场景。

  TestContext:将Key中关键字和具体的Word实现的函数进行关联,构造一个map,使得直接通过execute关键字就能调用起对应的函数。

  具体实现的代码我先不详细解释,我们先来看看使用这套框架后,之前实现同样功能代码写成了什么样子:  

  【Android测试】UI自动化代码优化之路(临时发布, 随时删除)

  可以看到,测试用例(这里认为一个test***函数就是一个测试用例)这一层我们做了高度的抽象,在testPublish这个函数中没有任何与开发源代码或者是资源id有关的信息了,这里的Key.EnterPublishPage就是我们的关键字,具体的实现在EnterPublishPage这个AW的函数中,这样写用例,当我们的界面发生了大的改变,例如我们版本迭代中从发布的两个页面,如下所示:

  【Android测试】UI自动化代码优化之路(临时发布, 随时删除)

  在新版本改变后,改成了一个界面,如下图:

  【Android测试】UI自动化代码优化之路(临时发布, 随时删除)

  可以看到界面元素的调整还是不少的,如果之前我们可能就得废弃之前的用例重新写了,但是使用了关键字驱动后,我们的用例层的改变根本不需要做任何的修改,而对应的如果控件ID改变后,我们只需要修改Word层即可。

  这里说下AW和KW之间如何封装,就代码层面,对于编译器来说,是没有AW和KW之分的,我们抽象这两层的意思就是,当有一个Word可以被多个用例复用的话,这样我们就把它封装起来供其他的Word使用。具体还需要在实践过程中慢慢的体会。  

封装测试框架


  说完了关键字驱动,需要说下封装框架这一块。看过我之前文章的朋友们应该知道我为什么要选择Espresso和Uiautomator,目前谷歌推荐使用这两款框架,有兴趣可以看看谷歌的这两篇原文:

  【Android测试】UI自动化代码优化之路(临时发布, 随时删除)

  那么两个框架同时加入到我们的测试工程应该如何去整合代码结构内,这里我自己使用这样的结构,觉得还不错的可以参考一下,首先先看下类图:

  【Android测试】UI自动化代码优化之路(临时发布, 随时删除)

  看完类图后可能有些人已经看出来啦,没错这里使用了简单工厂模式,具体的Word层来使用工程加工出来的对象,具体的工具封装内容包装在FrameUiautomator和FrameEspresso里。

  这里主要说下针对Uiautomator的封装,如果之前读过我写的《如何组织好你的测试代码》,那么里面的一些思想应该比较清楚了,主要的优点就是:

  1、页面跳转或者异步加载延迟出现的界面,无需再单独使用sleep;

  2、对于系统随机出现的可能会影响App界面的一些因素(例如Android6.0的授权弹框、电话呼入),无需再单独处理;

  3、对于App中随机出现的可能会遮挡正常界面的一些弹框,无需再单独处理;

  4、所有调用封装后框架的操作,都会记录日志;

  5、框架本身有断言能力,如果在框架处理异常情况后还找不到指定控件,这时候会截图并且断言;

  6、如果需要替换框架或者框架升级,可以使用最小的成本来框架层进行改动,而不需要改动用例层和Word层。

完善其他内容


  上面主要讲的是UI自动化的一些行为操作,关于断言的问题,我这里不想说太多,BTV做到界面上的UI元素的检查,以及整个流程是否可以完整的走下来就可以了,如果需要验证数据正确性等一些复杂的内容,可以参考我写的《App任你摆布(反射技术的引入)》。我这里说说UI自动化如果失败了,我们怎么排查问题?其实很简单我这里做的就是日志+截图。

  日志系统最关键的是打日志的时机,这里我把它埋到了BaseTest的execute()中,这样每一次的用例调用AW或者AW中调用KW,都可以记录下来,同时也埋到了框架的具体实现函数中,这样框架只要操作就会记录下日志。这里有个小的技巧,我在打印日志的地方调用了下面的函数:

String classname = Thread.currentThread().getStackTrace()[3].getClassName();

  这样就可以记录下当时调用Log函数的当前的类的名称了,这里可以看下我输出的log的样子:

  【Android测试】UI自动化代码优化之路(临时发布, 随时删除)

  是不是比较齐全了,基本上所有你想知道的信息都可以通过log内容来获得了。下面说说截图,截图和log的整体思路一样,会在一些关键节点埋点同时也支持手动调用。因为我的工具框架是支持自身断言的,因此我在工具框架这一层断言的时候会加入截图,其他地方如果你需要特别关注的时候,也可以手动调用截图触发。

  【Android测试】UI自动化代码优化之路(临时发布, 随时删除)

  上面的图是不是非常直观,当我们的用例出现异常错误的时候,直接通过log和日志即可定位到问题的所在。

结果展示


  测试结果最终对接了内部的持续集成平台和结果展示平台后是这个样子:

  【Android测试】UI自动化代码优化之路(临时发布, 随时删除)

  保证了编译器中的结果和结果展示平台中显示的情况一致。

结合实践谈谈


  互联网产品的迭代速度之快,各位都深有体会。做为产品质量的保障者,测试人员经常为测试时间不足而烦恼,如何打破现状来让现在变得更好一些,这是我们一直在思考的问题。软件工程中有提到测试人员越早的介入到研发的流程当中,就可以越早的发现问题,从而降低发现问题的成本。因此"左移"变得非常的有必要了起来,当然左移的方式有很多,例如前几天拜读到的《聊聊测试“左移”那些事》这里面主要讲测试人员通过把控需求来达到左移的效果,而我上面所讲到的内容都可以帮助自动化实现左移的。

  想想之前我们做的UI自动化是怎么做的呢?在版本提测之后,我们开始写自动化,这样自动化的主要功能就变成了回归和冒烟。我这里我想说的是在开发写代码的时候,我们也开始写用例级别代码,在开发定义了界面布局后,我们就可以完善具体代码,待开发提测时,我们就可以运行我们的用例来进行测试了。

  当然针对新老版本可能略微有所不同:

  如果是新需求的情况下,我们在需求确定的情况下就可以先组织自己的用例了,具体实现依赖开发的word层的代码可以先空着,待开发确定之后,我们就可以及时的完善我们的word层,这样不用等到开发提测之后,我们才开始设计我们的自动化测试用例。

  对于老的需求变更,同样也是,首先可以看之前的用例中的关键字是否有可复用的东西,如果可以直接复用,那就继续用,如果有新的步骤加进来,那么只需要加入对应的关键字即可,和新需求的做法一样,同样在开发提测之前完成用例的编写。

  其实在整个方案启动之前,我就在思考这个问题。那么这个做出来后究竟会有收益吗?因为毕竟做完整个系统不是一件容易的事情,需要花很多的时间成本进去。那么分析收益从哪方面入手呢?我觉定先从bug入手,于是针对最近的一次版本做了一个简单的bug分析:
  【Android测试】UI自动化代码优化之路(临时发布, 随时删除)
  从数据中可以看到,的确有一部分的bug是可以在左移阶段被发现的。这里分为BVT级别的用例和详细模块的用例。BVT级别用例来限制开发的提测,提测前开发自己去运动这部分用例,通过才可以提测;具体功能级别的详细模块的内容用专门针对这个版本修改或者新增的新功能。

  这就是本节想跟大家说的内容,整个过程中不管是对项目的收益或者是自己的成长,我都有所收货,拿出来和大家进行分享,希望能帮助到其他人。