找回密码
 会员注册
查看: 21|回复: 0

FlutterUI自动化原理与实践

[复制链接]

2万

主题

0

回帖

6万

积分

超级版主

积分
67441
发表于 2024-10-9 15:03:50 | 显示全部楼层 |阅读模式
Flutter UI自动化原理与实践 Flutter UI自动化原理与实践 姜璐璐 陈康 张超 贝壳产品技术 贝壳产品技术 “贝壳产品技术公众号”作为贝壳官方产品技术号,致力打造贝壳产品、技术干货分享平台,面向互联网/O2O开发/产品从业者,每周推送优质产品技术文章、技术沙龙活动及招聘信息等。欢迎大家关注我们。 242篇内容 2020年12月03日 23:25 1 引言Flutter是Google推出的跨平台UI框架,一套代码,多端复用,性能上可媲美原生。Flutter的诞生,引起业界的广泛关注,越来越多的互联网公司在APP中使用Flutter技术,像bat、tmd等知名国内互联网公司,研究和使用都比较深入。贝壳作为国内Flutter技术应用的先驱,2018年底就在Link&A+ App上线了Flutter页面,如今已有三百个Flutter页面上线应用;目前,公司共有10+款App在使用Flutter框架,占活跃App总数的80%。Flutter技术的引入,研发原来需要为Android、iOS编写两套代码,现在仅需编写一套,移动端研发的效率得到了大大的提升。但对于移动端测试同学而言,仍需对不同的平台做测试,工作量并没有同等比例减少。由于原有Native的自动化框架识别不到Flutter页面元素,Flutter页面也就不能通过自动化的方式进行回归测试;随着Flutter页面的增多,可通过自动化手段测试的占比随之降低,QA人工测试的成本越来越高。那么我们如何提高flutter页面的回归测试效率就迫在眉睫,于是我们开启了Flutter UI自动化方向的探索与研究。2 方案调研通过调研,目前业界自动化框架对Flutter的支持并不完善,不能满足我们的业务述求,基于此,我们从稳定性、业务接入成本、兼容性等方面进行调研、对比,最终自研了贝壳的Flutter UI自动化框架,并在租赁业务上应用。Flutter-driverFlutter-driver是Flutter官方提供的集成测试方案,有着丰富的API,但需要在业务代码中手动设置视图的ID,才能进行集成测试。对业务代码有侵入,另外,只支持Dart语言编写测试脚本,不支持Python,无法集成到KeMTC项目中,对于移动端测试同学增加太大的学习成本。Appium-flutter-driverAppium-flutter-driver是对Flutter-driver的封装优化。Appium是一种跨平台UI自动化测试框架,借助Appium的能力,可以用Python编写测试脚本;但是Appium-flutter-driver并没有解决Flutter-driver的需要手动设置视图ID的痛点。Airtest-图像识别Airtest是网易开源的UI自动化测试框架,通过图像识别的方式可以达到测试Flutter页面的目的,但是需要测试同学裁取很多图片并保存,由于与已有原生框架不同,集成到KeMTC项目中也是有成本的。经过详细的调研对比后,以上三种方案都无法满足业务的需求,尤其是在稳定性、低成本接入平台等方面。于是我们开启了Flutter UI自动化Ke-FUT(Ke-FlutterUiTest的简称)的自研之路。3 技术原理与实现3.1 概述Ke-FUT自动化的整体设计思路与原生相似,如图所示共有两个主要的步骤:获取元素ID:ID是指可以标示唯一页面元素的值。自动化测试脚本通过这个值去映射到对应的页面元素进行操作。驱动视图元素:针对对应的视图完成模拟操作,其中包括点击、滑动、长按、输入等。贝壳内部目前使用UIAutomator2(简称U2)框架实现Android原生的自动化测试,在U2中“分析页面元素”和“驱动视图”都是由Android SDK中的UIAutomator提供的能力。以此类比,Flutter实现自动化测试也需要提供类似的能力。3.2 项目架构架构设计如图所示,共分为3层:应用层、桥接层和服务层。应用层:提供了面向测试人员的使用接口,可以低成本接入到现有的自动化测试框架中。桥接层:将接受应用层的消息,调用服务层提供的服务服务层:运行在Flutter App中,向上提供分析页面元素,驱动视图等能力3.3 获取ID的原理及实现Ke-FUT获取元素ID是借助了Flutter VM Service的能力,对关键方法进行修改,以达到获取元素ID的目的。VMService和InspectorService是Flutter SDK提供的服务,其主要作用是帮助开发人员检查页面结构,从而在视图布局出现问题时快速定位原因。利用其稳定的页面结构分析和元素定位,我们可以轻松的获取到元素ID。VMService在Flutter初始化时开启,我们可以通过脚本启动InspectorService去连接VMService。成功连接VMService之后,发送“show”消息使得Flutter页面进入SelectedMode模式,当前页面元素即可被选中。InspectorServiceinspectorService;///测试注册InspectServiceFuturemain(Listargs)async{//url即是VMService的服务地址finalStringurl=args[0];finaluri=normalizeVmServiceUri(url);FrameworkCore.init(url);//连接VMServicefinalconnected=awaitFrameworkCore.initVmService('',explicitUri:uri,errorReportermessage,error){});if(connected){//创建InspectorService去监听inspectorService=awaitInspectorService.create(serviceManager.service).catchError((e){},teste)=>eisFlutterInspectorLibraryNotFound);}else{safe_exit(1);}//发送消息,使得Flutter进入SelectedMode模式awaitinspectorService.invokeServiceMethodDaemonNoGroup('show',{'enabled':true});}在SelectedMode模式下,点击元素会触发VMService的WidgetInspectorService类中的_getSelectedWidget函数,该函数将Element的调试信息返回给InspectorService。我们使用AspectD(闲鱼开源的Dart AOP框架)hook此函数,在其返回的Json数据中增加了ID字段(具体实现原理在3.4小节详述),从而实现Element ID的监听。@Execute("package:flutter/src/widgets/widget_inspector.dart","_WidgetInspectorService","-_getSelectedWidget")@pragma("vm:entry-point")Map_getSelectedWidget(PointCutpointcut){print('call_getSelectedWidget');//在selectedMode下当前选中的ElementfinalElementcurrent=WidgetInspectorService.instance.selection.currentElement;Mapmap=pointcut.proceed();if(current!=null){//将Element映射成对应的IDmap['autoId']=IdGenerator.idGenerator(current);}returnmap;}在InspectorService获取到Element ID之后,“页面元素分析器”只需要通过WebSocket和InspectorService建立连接就能将Element ID展示出来。从而实现了分析页面元素并获取对应元素ID的过程。3.4 ID_generator的原理及实现ID_generator是将Flutter中的页面元素Element映射成ID的工具。在上一步中获取的ID值是Element通过ID_generator映射而成。Element List是通过当前Element调用visitAncestorElement函数递归访问父Element获取的Element集合。在Element映射成WidgetName的过程中将一些不会影响创建路径的Element舍弃。classIdGenerator{///ElementList映射成WidgetName的过程staticvoid_parse(ListallList){for(inti=0;ichanList,Stringtype){......MultiChildRenderObjectElementmultiChildRenderObjectElement=chanList[0];inti=0;intfinalIndex=0;multiChildRenderObjectElement.visitChildren((varelement){if(element==chanList[1]){///childelement等于其中children中的一个,获取indexfinalIndex=i;}i++;});idList.add('$type[$finalIndex]');}}3.5 驱动视图的原理及实现在获取Element ID之后,我们的测试脚本可以使用Element ID通过Ke-FUT驱动对应的视图元素。而驱动视图的过程实际上就是FUTClient和FUTService通信的过程。3.5.1 FUTServiceFUTService运行在Flutter App上,通过WebSocket连接FUTService可以获取对应Element的相关信息或对Element进行断言、输入等操作。FUTService在Flutter App启动时开启,通过WebSocket监听4567端口。连接FUTService,就能获取对应的Element的信息及驱动Element。目前支持如下API:getPositionById:通过Element_Id获取元素相对于屏幕左上角的绝对坐标getPositionByText:通过文案获取元素相对于屏幕左上角的绝对坐标setText:通过Element_Id找到对应的TextField并设置输入值assertText:检测Element_Id对应的元素是否展示对应的文案3.5.2 FUTClientFUTClient通过WebSocket调用FUTService的服务,完成驱动视图并向上提供可供测试人员编写自动化脚本的API。现有的自动化测试框架可以通过实现FUTClient完成Flutter App的UI自动化测试。目前我们使用Python语言实现了FUTClient,并将其集成到贝壳内部的KeMTC平台中,接入过程简便,成本低。FUTClient实现提供如下API。3.5.3 Click_id的实例FUTClient实现defclick_id(self,element_id,logtext):""":paramelement_id:元素id:paramlogtext:打印log文案"""ifelement_id:position_map=flutter_client.get_position_by_id(element_id)print("ID-{}的坐标值为:{}".format(logtext,position_map))ifisinstance(position_map,dict)andposition_map.get("x")!=0:x=position_map["x"]y=position_map["y"]self.d.click(x,y)logger.info("点击元素:{}".format(logtext))time.sleep(2)elifposition_map.get("x")==0andposition_map.get("y")==0:logger.info("ID:{}返回坐标值为:{},元素不存在!".format(element_id,position_map))raiseAssertionError("ID:{}返回坐标值为:{},元素不存在!".format(element_id,position_map))else:raiseAssertionError("元素异常:{}".format(logtext))#向server端发送请求接收ID相对位置[Map]#[id]界面元素iddefget_position_by_id(self,id:str)->dict:id_en=id.encode("utf-8")base64_id=base64.b64encode(id_en)tem_map={'method':'getPositionById','id':base64_id}json_map=simplejson.dumps(tem_map)#发送请求self.client.send(json_map.encode('utf-8'))whileTrue:#接收数据data=self.client.recv(1024)#读取消息ifnotdata:breakposition_map=simplejson.loads(data.decode('utf-8'))returnposition_mapreturnNoneFUTService实现///通过id获取其在界面中的具体位置[Map]///[id]界面元素idMapid2Position(Stringid){//获取当前页面的所有的ElementinitElement2IdMap();if(element2IdMap.containsKey(id)){//通过ID映射成对应ElementElementelement=element2IdMap[id];finalRenderObjectrenderObject=element.renderObject;if(renderObjectisRenderBox){//偏移Offsetoffset=renderObject.localToGlobal(Offset(0,0));//当前元素大小Sizesize=renderObject.size;doublex=(offset.dx+size.width/2)*window.devicePixelRatio;doubley=(offset.dy+size.height/2)*window.devicePixelRatio;return{'x':x,'y':y};}}return{'x':0,'y':0};}4 Ke-FUT实践4.1 获取元素Flutter工程根目录下,执行 flutter attach,进入App,复制链接利用flutter_devtools工程 ,执行dart auto_test_main.dart “复制的连接”打开bk_weditor,点击Connect Flutter ,点击show,操作界面即可看到flutterId(如图)4.2 编写用例以贝壳租赁的调价功能为例,用例脚本如下:deftest_puzu_change_price(self,init):#点击调价文案self.flutter_base.click_text("调价")#根据ID点击self.flutter_base.click_id(elements.price_icon,"调价")#在指定ID处输入文案self.flutter_base.set_text(elements.price_icon,"322")#点击保存文案self.flutter_base.click_text("保存")#断言Toastself.base.assert_toast("调价成功")执行用例效果:5 Ke-FUT未来展望目前Ke-FUT项目已经在贝壳租赁业务上接入,并覆盖多种业务。而且Ke-FUT已集成到KeMTC的Android和iOS自动化项目中。从使用上看,Ke-FUT支持常见的自动化操作并且运行良好,但是整体上仍然处于起步阶段,这也是我们对Flutter UI自动化测试的初步尝试。后续我们会支持元素ID、文字等模糊匹配,以适应更多测试场景,同时也欢迎移动端的测试同学感兴趣可以联系我们,多多试用,提出宝贵的建议~ 预览时标签不可点 Flutter15QA8移动端37Flutter · 目录#Flutter上一篇如何玩转Flutter动画下一篇Flutter图片内存优化实践关闭更多小程序广告搜索「undefined」网络结果
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2025-1-3 01:19 , Processed in 0.732960 second(s), 25 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表