必要性
每一次软件发布新版本的时候,新的功能模块可能与旧的功能模块产生冲突,而导致原来的功能出现Bug,所以每次发版前都要做一次回归测试以保证原来的功能可以正常使用,而每次的回归测试都产生了重复的劳动力。为保证软件兼容性,每次的测试都需要在不同的平台上进行测试,而当前手机等Android设备五法八门,各种牌子,各种型号,所以往往要在多款手机上进行相同的测试来保证兼容性。显然,这些大量的重复劳动力由自动化测试来完成就很有必要了。当下最炙手可热的自动化框架非Appium莫属了,由于它具有跨平台性、多语言支持和技术社区活跃等优点,所以我们选择了它来搭建自动化框架。
易用性
既然要搭建一套自动化测试框架,那么就必须做到完全自动化,尽可能减少人工操作,提高易用性,完全自动化包括
①,自动采集设备信息,无需手动获取,当USB口接入一台新设备可直接开启自动化测试工作;
②,自动配置信息,无需手动配置,摒弃TestNG群控时需要手工配置多个suite.xml的方式;
③,自动启动Appium服务,无需手动打开,由自动化工作开始的时候通过代码打开;
④,自动安装新版本软件,无需手动安装,由自动化工作开始前通过对比版本进行软件更新。
接下来针对以上4点的实现流程做简述:
①,在做Appium自动化之前,我们是需要配置设备名和版本,往往我们通过adb指令来获取,然后填上去,虽然也是简单的两句指令,但是每次接入一个新手机都要获取一次也是有点麻烦,所以为什么我们不让代码来获取并自动获取呢?我们知道代码是可以直接执行adb指令的,所以我们只要在每次执行测试前先执行我们的获取设备信息的代码,那么就不用手动再敲了。具体实现如下,分别为获取设备名和根据设备名获取版本。
public static List<String> getDevices(String adb) { List<String> devices = new ArrayList<>(); List<String> results = RuntimeUtil.exec(adb + " devices"); if (results.size() > 0) { for (int i = 0; i < results.size(); i++) { String deviceName = results.get(i); deviceName = deviceName.substring(0, deviceName.indexOf("\t")); devices.add(deviceName); } } else { new Throwable("Can't find devices").printStackTrace(); } return devices; } public static String getPlatformVersion(String adb,String deviceName) { List<String> results = RuntimeUtil.exec(adb + " -s " + deviceName + " shell getprop ro.build.version.release"); return results.get(0); }
②,获取完配置信息,就需要把信息设置进去了,以往我们是通过填在一个xml配置文件里的:
<suite name="Suit1"> <parameter name="port" value="4723" /> <parameter name="bootstrap_port" value="4724" /> <parameter name="chromedriver_port" value="9515" /> <parameter name="udid" value="Q505T" /> <parameter name="node" value="node" /> <parameter name="appiumMainJs" value="C:/Users/dell1/AppData/Local/Programs/appium-desktop/resources/app/node_modules/appium/build/lib/main.js" /> <parameter name="platformName" value="Android" /> <parameter name="platformVersion" value="4.3" /> <parameter name="deviceName" value="Q505T" /> <parameter name="appPackage" value="com.tencent.mobileqq" /> <parameter name="appActivity" value="com.tencent.mobileqq.activity.SplashActivity" /> <test name="testqq"> <classes> <class name="testqq.TestMessage" /> </classes> </test> </suite>
单独的一个配置文件,很直观也方便,但是需要手动配置,那么有没可能手动生成这个文件呢,当然是可以的,但是生成一个文件是通过java操作io流的方式,而这种方式是比较耗时的,所以还有没其他方式呢,想到这个xml配置文件最终也是会被解析成一个对象的,那么我们直接根据信息构造出一个对象不就可以了吗,而且,通过查看代码,也发现了testNG是支持构造对象的形式来配置的,通过setXmlSuites方法可配置一个XmlSuite列表进去。
接下来我们看看如何配置一个XmlSuite,通过xml文件的配置方式和testNG的源码,大致理清其结构如下
当然class里还有最小的测试单元method。以上元素属性缺一不可,基本上与Xml配置文件相似,比较坑的一点就是XmlTest必须指定其所属的XmlSuite,否则会报空,这点在配置文件里是看不出来的,需要根据testNG源码才能找出来。既然知道了XmlSuite对象结构,那么接下来就可以编写构造代码了,这里我编写一个XmlSuiteBuilder来构造:
public class XmlSuiteBuilder { List<XmlTest> mTests = new ArrayList<>(); Map<String, String> parameters = new HashMap<>(); XmlSuite xmlSuite = new XmlSuite(); public XmlSuiteBuilder(int index, String deviceName, String platformVersion, Configure configure) { parameters.put("port",(4723+2*index)+""); parameters.put("bootstrap_port",(4724+2*index)+""); parameters.put("chromedriver_port",(9515+index)+""); parameters.put("udid",deviceName); parameters.put("platformName","Android"); parameters.put("platformVersion",platformVersion); parameters.put("deviceName",deviceName); parameters.put("node", configure.getNode()); parameters.put("appiumMainJs", configure.getAppiumMainJs()); parameters.put("appPackage", configure.getAppPackage()); parameters.put("appActivity", configure.getAppActivity()); parameters.put("app",configure.getApkPath()); parseTestBeans(configure.getTestBeans()); xmlSuite.setName(deviceName); xmlSuite.setTests(mTests); } private void parseTestBeans(List<TestBean> testBeans){ for(TestBean testBean : testBeans){ String testName = testBean.getName(); Class[] testClasses = testBean.getClasses(); List<XmlClass> xmlClassListt = new ArrayList<>(); for (Class testClass : testClasses) { xmlClassListt.add(new XmlClass(testClass)); } XmlTest xmlTest = new XmlTest(); xmlTest.setName(testName); xmlTest.setClasses(xmlClassListt); xmlTest.setXmlSuite(xmlSuite); mTests.add(xmlTest); } } public XmlSuite build() { xmlSuite.setParameters(parameters); return xmlSuite; } }
构造好XmlSuite后就可以通过setXmlSuites给testNG设置进去了。
③,通过代码执行命令行来启动Appium,而不是点击桌面图标的方式:
if(!RuntimeUtil.isProcessRunning("0.0.0.0:"+port)) { new Thread(new Runnable() { @Override public void run() { String cmd = nodePath + " \"" + appiumPath + "\" " + "--session-override " + " -p " + port + " -bp " + bootstrapPort + " --chromedriver-port " + chromeDriverPort + " -U " + udid; RuntimeUtil.exec(cmd); } }).start(); SleepUtil.s(10000); while(!RuntimeUtil.isProcessRunning("0.0.0.0:"+port)){ SleepUtil.s(2000); } }
④,自动安装新版本Apk的思路是,首先设置好Apk路径,每次安装前通过AXMLPrinter2检测路径下的Apk的版本,然后再检测手机中安装好的Apk的版本,通过对比版本来判断是否要更新。更新通过adb install指令即可。这里本想通过adb shell dumpsys package 这条指令来获取手机中Apk的版本号,但是发现只有部分手机才能获取成功,所以得换一种方式,那是什么方式呢?做过安卓开发的都知道安卓应用是可以用PackageManager 通过包名获取到Apk的信息的,所以这里我的想法就是往手机上安装一个工具Apk,然后通过这个Apk来取到目标Apk的信息,然后写入文件中,最后adb pull把文件复制到电脑解析出来,这样子获取出来的不仅仅只有版本号,还有一些其他有价值的信息,这些信息是通过adb获取不了的。
代码分模块、分层
我们应用基本上都是按模块来划分的,比如登录模块,消息模块,充值模块等,所以我们的用例一般也是按照模块来编写测试,同理,对应自动化测试上面也是根据模块来划分的,每个模块可能有上百条用例,上百条用例不可能写在一个类里,所以需要再对模块进行一次划分子模块,其对应关系如下:
XmlTest → 测试模块 → 包
XmlClass → 测试模块的子模块 → 包里面的类
method → 子模块中的case → 类里的所有方法
代码分层主要分为元素定位层和逻辑操作层,我们平时的操作可能会把元素定位和逻辑操作写在一个方法里,这样当一个页面复杂的话会导致这两种代码混在一起,不易查看和维护,所以我们得做好代码分层,这里我们把元素定位剥离出来,采用如下注解的方式来实现元素定位。
@FindBy(id = "com.tencent.mobileqq:id/ivTitleBtnRightText") WebElement element;
遇到比较复杂的,比如查找TabWidget里的所有FrameLayout,依然可以用注解的方式查找
@FindBys({@FindBy(className = "android.widget.TabWidget"),@FindBy(className = "android.widget.FrameLayout")}) List<WebElement> list;
自动化性能测试
以往在对App的性能测试的时候,往往使用一些现成的测试工具,比如GT,Emmagee,Mat等,然后手工操作一遍功能流程,从而生成报告进行分析,然后这种方式有几个局限:
①,测试过程依赖于手工;
②,测试结果只能看到整体性能走势,如果出现性能占用的高峰,则不能确定具体是哪些操作或者哪个页面导致的;
③,因为这些工具大多采用的是RunTime来执行命令获取被测应用的性能数据,比如top命令获取pid、访问/proc命令,而Google爸爸已经在7.0以上的系统禁用了这些命令,所以这些工具在非Root的情况下只支持7.0以下;
④,只能测试cpu,内存,流量,电量等,无法深入到应用里检测并收集内存泄漏日志,卡顿日志,crash日志,并注明是哪些操作或者哪个页面造成的;
而现在,我们可以做到突破以上四个局限:
针对①,我们在自动化测试功能的同时,同时也进行着自动化的性能测试,这样就可以抛弃手工测试性能的方式;
针对②,在测试同时每采集一次性能数据就记录当前执行的是哪个case,这样出现性能占用高峰的时候可以知道是哪个case造成的;
针对③,这里我们采用adb来获取性能数据,包括cpu,内存,流量数据。adb目前的权限还是很高的,不会存在7.0以上不支持的情况;
针对④,编写测试应用,测试应用集成内存泄漏检测,卡顿检测,crash检测,采用Instrument方式使得测试应用和被测应用跑在同一个进程,然后开启内存泄漏检测,卡顿检测,crash检测,出现以上问题则把相应的日志信息发送回主机,主机接收到就获取当前正在执行case,并在最终的报告中体现出执行什么操作,出现了什么异常,具体是在哪行代码造成的。
为了实现兼容不同的待测应用,需要对测试应用Apk进行动态修改,考虑到每次重新编译比较麻烦,这里通过逆向修改。
报告定制
最终测试完成需要生成一份报告,报告所展示的信息要求直观明了,清晰自然,所以这里使用了体验更好的ReportNG,并对其做了定制修改。①,添加case的执行时间;
②,添加失败截图,截图点击放大;
③,添加case作者信息,case失败了,需要查询原因方便找到责任人;
④,添加失败重跑策略,其中没有开启网络不重跑,crash、anr不重跑直接采集crash或anr日志输出到报告,其他情况执行重跑,重跑的case还是失败了会被testNG标记为多个skipedTest,需要进行去重,去重后把最后一个skipedTest的runCount赋值给passedTest或failedTest,最终显示在报告上是passedTest或failedTest的运行次数,而没有skipedTest;
⑤,添加图表信息,包括测试结果圆饼图,和测试过程中应用的CPU,内存和流量的曲线图;
⑥,本地化,添加多一份reportng.properties_zh_rCN,需要注意的是里面的中文需要转成ascll码,否则打出来的包会乱码。
最终生成的报告如下图,当然后面添加功能还要进一步完善
展望未来
虽然框架已经做了大部分的优化工作,但是还存在一些不足,以及需要优化的地方①,框架暂未兼容iOS,但由于Appium本身兼容iOS,所以未来要实现也是有据可循的;
②,实现通过用例的导入,就可以自动生成代码去实现自动化测试。例如在一份Excel测试用例里,每条用例增加一项自动化脚本,而这些代码可以通过脚本录制生成代码来写入,然后导入这份用例,框架可以收集用例信息自动生成测试类,最终实现无需编写代码即可进行自动化测试;
③,未来考虑兼容更多优秀的插件,实现插件的可拔插,比如接入自动遍历插件AppCrawler,安全检测插件,接口Hook插件等;
总之,一句话,任重而道远。