本文紧密结合移动开发方法与技术,围绕Android平台的开发探讨提供更高质量移动产品的解决方案。作者中分析了移动开发中常见的问题,从两方面阐述了ThoughtWorks使用的测试开发方案和相应的架构方法与常用工具应用,并进一步阐述了为移动开发流程所提供的持续发布方案。
随着云计算、移动互联等一系列新技术概念的崛起,新一轮的IT经济正在不断扩大发展。带来无限机遇的同时,也提出了许多有别于传统开发的挑战。近几年来,我一直在尝试各种移动项目,虽然它们在应用领域、技术类型以及工作模式等方面各不相同,但我在摸索中逐渐总结出了一些比较具有共性的问题。
移动项目中的常见问题
为了实现较好的用户体验,反复的设计与验证导 致产品发布时间延长。移动应用由于其多样性的应用场景,使产品设计侧重于适应不同目标的展现方式与操作习惯。设计实现的方式与使用用户群、企业服务模式、新的科技实现手段,以及各种碎片分化的目标支持设备等一系列因素密切相关。在产品实现初期,许多的内容和形式都需要在已有开发原型的基础上,进行紧密结合用户体验的测试。不少团队花了很长时间完成了目标,可测试后又要经历反复且大量的修改。这对产品的如期发布提出了巨大的挑战,发布时间也会因此一拖再拖。
即便是产品磕磕绊绊地发布了,设计的改变往往也是最让人头痛的问题。
图1 某产品四个阶段首页面对比图
市场导向性强,业务需求变化快与缩短产品交付周期需求之间产生矛盾。经过了一系列的市场分析、产品设计、项目研发过程后,一个移动产品终于投放到了市场。但这完全不能看做是一个项目的交付完成,恰恰相反,这只是一个新阶段的开始。在残酷的市场竞争中,用户不是产品的被动消费者,而是需求的提出者,会在各种下
载市场(例App Store、Google Play)发出评论对应用进行评估,从而直接影响应用的市场占有率。同时随着一系列用户体验数据的收集分析和整理,业务部门的需求递增,新功能点开始一个
个被搬上研发经理的台面。这给开发团队应对需求改变以及对递增的代码结构升级能力提出了更高要求。图1展示了一个产品在过去一年多时间里首页面的变化。
可以看出在项目所经历的四个比较大的阶段中,仅一个首界面的功能,也经历了从最开始的普通列表界面,到后来增加地图功能、书签的过程。在第四个阶段中,为了满足用户注册登录等需求,首页面还进行了基于左侧滑动菜单的导航转型与登录反馈等的功能扩充。每个重大阶段的转型,都来自最真实的市场评论数据,并结合
Omniture(通过收集用户数据行为分析的工具)等产品的体验数据分析与业务增长需要进行开发。
为了实现更加精细的体验效果并兼容 Android的各个版本(例如图1中列表和地图间的切换需要通过动画三维翻转实现等),所有这一切都必须在同一个Activity内完成交互。这给大量的页面逻辑、状态和视图层级关系的升级改造带来了很大的难度。与此相对应的坏消息是,移动应用对短周期的快速发布有着强烈的需求。即使是一个好的应用,如
果没有及时保持稳定频率的更新,很快就会被接踵而来的竞争者追赶,最后落到被用户遗忘的境地。从某种角度讲,除了产品新功能的推出外,应用程序的更新也具
备某种广告的职能,去强化品牌在消费人群中的地位,稳定和扩大市场的占有率。
移动团队虽小,但要求更良好的产品集成性。同一个产品,一般根 据支持平台的数量以及并行发布需求,配置有多个小团队和数据整合(API)团队。每个团队的开发因为进度不同和平台特点的不同,往往在整合过程中提出各自不同的集成需求(包括数据集成和逻辑集成),例如Android的内存性能不好,要求服务端的图片质量与剪裁要和iOS有所区别;再例如有时为了降低网络
性能对体验的影响,会更改设计,将逻辑分散在API整合段和设备端。这就为团队间的整合埋下了风险。事实上,这在多团队的并行开发中,并不是个别现象。
多样化的设备和版本、长期的维护开发,带来快速升高的测试成本。随着开发功能的增加,页面布局、操作响应和交互处理大量逻辑模块增加,以及越来越分化的设备
和系统版本,给测试工作带来了相当大的难度。在之前我做咨询时,一个项目经理告诉我,他有一个运行了一年半的项目,当时还有一周就要上线,可仍有60个Bug,5名测试工程师因为对质量没有信心而不停加班,开发也为修改一个个错误都头痛不已。
通过技术方案寻求解决途径
为了解决上述常见移动项目的问题,在项目实践中,我们试图通过合理地运用技术方案来帮助完成高质量移动软件的目标。
实现高可维护性的代码,减少代码扩展过程中的腐化和变动带来的副作用。随着功能增加,越来越多的逻辑模块被堆砌在同一个单元内,导致代码可读性下降,维护复杂度提高。这时的修改都存在破坏原有功能的潜在风险。
如果解决这两个问题,将在很大程度上提高应用在开发过程中对产品需求改变和开发周期控制的适应能力。实践中,我们使用元素组件化开发和测试驱动开发解决方
案。组件化的基本目的就是将代码的可读性置于编码过程中,通过形成独立子元素组件来代理自身的功能逻辑,并以此结构化XML的布局资源,提高可读性。同
时,通过测试驱动的开发方式为代码的粒度质量提供原始保障,让代码演进过程减少编码副作用破坏其他功能的现象,从而提升代码的可维护性。
通过功能自动化测试,保证开发过程中测试成本的相对稳定和质量保障。这里要分两个部分来谈。第一部分是通过提高自动化测试的比例,减少由人工重复完成的测
试。在应对多平台、多版本、反复回归测试时,自动化测试对质量的保障就显得尤为重要;第二部分是由于对需求理解偏差,产生的质量问题和因此增加的返工。这
里就需要引入基于业务行为驱动开发(BDD)的自动化测试管理。让需求成为可验证的执行代码,将会巨大限度的缩小业务需求、开发和测试之间的鸿沟。
当然,我在从事多个项目开发咨询的过程中,也曾遇到大量的需求变更,导致自动化测试废弃,从而提高成本的案例。这时往往要注意协调自动化测试金字塔,即单元测试、功能测试、UI界面测试等几部分的比例关系,实现质量与成本的平衡。
让随时可工作的产品来提高团队的交付能力。面对不断变更的需求与任何时候都可能出现的产品延时,提高团队的整体交付能力,就显得格外的重要。作为重要一环的持续集成和可用多点环境下测试,便成为这其中不可或缺的重中之重。而如果产品的各个平台都可以保证相对稳定的持续集成与发布,那么这样的方案也自然成为消
除团队合作壁垒的重要技术保障。
加快用户体验验证周期,增加修改频率降低单次修改的调整规模。优秀的用户体验,永远是前端工程师最关心的部分。这就需要能够尽早地去做更深入的分析与验证,更快地发现问题,并接进行修改与调整。同时缩短反馈回到开发端的周期,将大大降低代码的修改难度,提高产品效率。而自动化的编译、集成与部署操作流程就是用一个非常有效的方法来完成闭环回路。可持续集成与部署技术为缩短周期提供了根本上的保证。
刚刚提到了许多用来移动项目问题的解决方案。那么下面就让我们来看看在Android开发领域,这些方案是如何具体实施的。
结构化组件
所谓组件化就是将应用内部的UI元素充分拆分成相互独立子部件(例如常见的Android的Widget组件)。大的子部件由多个小的子部件组成。
这样的好处是除了在代码层面上更易于修改外,同时也通过对类和方法的命名实现了XML代码的结构化。图2作为一个简单的示例,大致表现了图1中第四个阶段首页面的组成方式。可以看出XML代码中的每一个子组件都是一个相应的视图组件,这些组件通过Java类和XML的对应完成一个子视图功能。XML中大体可
以表达出页面视图的一个可读性组成方法,而对应的Java文件则代表了每个视图的生成细节模型和交互响应逻辑。
另外需要说明的是,我们曾尝试了多种消息传递机制。最后证明,组件的单一顺序传递是一种相对最稳定和易理解的传递方法,子视图的交互消息只能传递给其父视图,然后由父视图传递给其他子视图。即如图2中ContentRotableScreen接收到一个自己不能处理的响应事件,应该将消息传达给SlidableContainer,
再统一分发给需要处理事件的NavMenuScreen作相应的动作。
图2 某产品四个阶段首页面的组成方式
这样做既保证了消息传递的准确性,又维护了一个统一的代码结构,方便实现代码的可读性和可维护性。
单元测试工具
单元测试是测试驱动开发的主体测试构成,旨在从代码粒度上实现对应用质量的把握,是可维护性代码的核心。其具体粒度大小取决于在代码出现问题后,能在多大程
度上准确定位问题。这也是单元测试最大的意义所在。这份意义所带来的是更高的代码可维护性、更稳定的代码可重构性、更便捷的可扩展性。而这一切为稳定结构
变化、减弱代码腐化影响、技术改进所带来的代码变更奠定了良好的基础。
为了实现比较良好的单元测试,需要一系列代码结构优化和测试工具使用的辅助。
在做深入说明Android系统开发结构之前,先来看一下在该平台开发时所常见的工具和相应的优缺点对比,如表1所示。
表1 各测试工具优缺点比较
根据现有经验,我们曾尝试了表1中四种单元测试方法。很难说哪一个方案是最好的,在目前的项目实践中,我们联合应用Robolectric和Java
JUnit,为了避免Robolectric速度、模拟功能不全和质量检测工具等问题,需要对Robolectric的代码进行修改,并尽量减少对其的应
用。转而通过一些结构上的应用,大量采用Java JUnit测试。
JUnit。鼎鼎大名的Java测试框架,无数应运而生的mock框架支持,使其无论是可用性还是易用性方面在Java领域无人能及。利用JUnit可以实现非常快捷单元测试。也是最常见的一种单元测试形式。
Robolectric。这是由Pivotal Labs开发的一套开源的Android单元测试框架。其通过一系列对底层Android元素的替换来实现对原有元素调用的模拟,从而实现脱离模拟器的测
试。非常值得一提的是,在测试服务器请求时,Robolectric的数据模拟和延时发送模拟,给多线程状态下的测试提供了很好的解决方法。
Robotium。 因为其对整体应用的黑盒操作特性,绝大多数技术文章将其作为功能测试工具,因此后文在叙述功能测试时也有提及。但因为其已有代码库,进行测试前需要组合编
译,而且可以完成方法级别的功能测试,因此本文还是将其描述重点在单元测试时和其他工具进行对比说明。但并不等于它就是单元测试。
Android JUnit。这是一种最常见的单元级别测试。它由Android官方提供,通过虚拟机自身提供的测试接口完成。图3源于Android开发者官网,基本上
阐述了整个测试框架各个组成部分。其中,最下面方框中描述的即为框架中基于JUnit的测试部分。可以看到,其通过Android的内部测试包,调用测试
执行模块完成对目标应用的测试。
该测试最大的好处是其与Android系统结合紧密,贴近真实环境。但其弊端也正是因为使用大量基于平台的虚拟,导致测试运行速度相对偏慢,影响测试效率。又因为开发过程中,单元测试是最大,也是最常规的测试,所以这种影响带来的效率降低就显得特别严重。
图3 Android JUnit测试框架
小结
本文我们介绍了移动开发中的常见问题、技术解决方案的基本思路,以及在具体实现中所要涉及的结构化组件和单元测试工作。在下期《程序员》中,我将继续讲解技术方案的具体实现方法,包括如何通过框架选取实现测试驱动方案,业务行为驱动的功能测试方案、持续集成、部署,以及如何让调试可持续化。
本文延续上期话题,深入到测试、持续集成和部署等环节,紧密结合移动开发方法和技术,围绕Android平台的开发讨论提供更高质量移动产品的解决方案。
通过清晰的架构实现测试驱动
通过《程序员》杂志9月刊文章的分析,我们可以看到,每一种工具都很难从完全意义上解决工程当中追求快速和高质量的要求。那么就需要通过整体架构实践,更好地解决这方面的问题。以下两种结构方法可供参考。
平台和领域的分层结构
如图1所示,这是一种最基础的分层结构。它将和Android平台相关的内容都包裹在了平台层,而将领域、状态、逻辑等和平台无关的内容完全隔离开来。创建项目时,将平台层和测试层分别对领域层产生依赖,以这样的方式达成对项目和对测试的独立构建。
这样做的好处是,领域层可以通过Java的JUnit进行完全测试,而项目各个类之间的因为有明显的项目级别分层,使得代码的考量更加清晰,方便于检验最核心关键的业务逻辑。且测试速度非常快,便于快速迭代。
但其带来的不便之处就是,随着项目代码的日益复杂,其与Android内部结合就越紧密,这时往往越难将业务逻辑很清晰的剥离。部分的业务逻辑将被分片散落在一些平台相关的调用当中。因此我们认为这样的一种层次逻辑更多地适用于简单的项目之中。
图1 平台和领域的分层结构
MVP(Passive View)架构
许多文章对Passive View是属于MVP还是MVC有各种不同角度的争论。这里我并不想过多地在词语上做过多纠缠,主要是希望通过描述这样的一种结构来树立比较好的对于Android开发的架构模型。
具体到Passive View的实现,如图2所示,描述了一个基本的原理。
随着项目的不断扩大复杂化,我们需要更加清晰化的代码架构。所以,Passive View对平台和领域的分层结构的层次结构有了更深层次的改进。引入Passive
View模式的意义实际上还是从测试出发。出于对JUnit快速性能的青睐,所以依然尽量把层次划分为依赖Android内核的部分和非依赖部分。对于前
半部分,可以用基于Roboletric的上层测试完成,相对应的后半部分则可以使用标准Java进行单元测试。这样做既加快了测试速度,又得以保障测试的覆盖率。
图2 Passive View架构实现原理图
更多详细的架构资料,请大家参考《GUI Architecture》一文,相信会得到许多很好的想法。
测试驱动开发的实现
之所以要强调代码框架结构,最重要的原因就是寻找快捷方便的途径进行TDD开发。在上一篇文章中已提到了Android常见的集中单元测试工具。之所以将框
架设计方式引入测试驱动开发过程,目的是将应用代码分开处理、分而治之。最大限度让各工具间扬长避短。最终实现高效率测试驱动的目的。
事实上,在目前的开发领域,我们也看到了许多正在应运而生的开发框架,例如Android Spring框架。这些都是下一步需要努力和完善的目标。
业务行为驱动的功能测试方案
如果说单元测试驱动开发帮助我们从开发角度完成了对代码质量的控制,那么基于业务行为的功能测试则为从业务到实现的统一、软件质量的保障提供了重要的解决方法。
功能(验收)测试:就功能测试而言,所指基本上就是测试科目中常提到的黑盒测试。只不过这里加上了更多具有上下文的信息,使得一次完整的黑盒测试更具有实质上的功能意义。
在 Android的开发中,通常是模拟真机或模拟器的用户操作(例如点击或滑动),来检测操作的结果是否符合预期。这个过程可以很好地代替人工的操作,更快速和大量完成重复性的测试工作,特别是在发展较快、平台和系统版本覆盖较广的项目中,可以起到节约人力成本、提高测试覆盖与准确性的作用。
在常用的工具中,可以选取上一篇文章中介绍过的Robotium或Native Driver来实现。
前者社区发展速度快,实现功能相对更加完善,例如实现了一些手势操作效果等。不过问题是Robotium的组织目前正在倾向闭源化,有些最好的新功能已无法在开源版本中寻找到。
后者的原理是通过植入应用后门的方式,实现对应用的发出指令和进行资源获取。其物理模型比较适合不能再目标设备安装测试应用,或者希望远程控制的情况。开源状况良好,只是近一年,Native
Driver的团队把重点发在其他平台上,对Android的更新比较慢。
基于场景的的跨平台验收测试:基于场景的验收测试,好处是可以在业务人员和开发人员之间建立验收机制。通过一系列关于场景的自然语言描述,完成测试的原始编
码。Cucumber是一个在行为驱动开发(BDD)领域比较常见的工具。其基本方法是通过执行文字形功能描述语言来实现对软件的自动化测试。图3展示了
一个常见的Cucumber测试场景。在Android开发中,通常是解析Cucumber,根据内容分情况拆分正具体的操作步骤。将这些步骤用基于
NativeDriver的Java代码进行实现,通过NativeDriver从指令端到设备端的Android自动测试框架完成控制与校验的自动化测
试。
图3 常见的Cucumber测试场景
当然,这些步骤也可以通过Robotium的远程控制(RC)调用来实现。其原理类似,如图4所示。
该方案还有额外的好处—用同样的方法,通过Cucumber可以实现对UIAutomation等其他平台自动化测试工具的控制。实现一份用例,多个平台的测试解决方案。图5给出了一个iOS平台上的基于Cucumber的跨平台测试实例。
图4 Cucumber测试实例
图5 iOS平台上基于Cucumber的跨平台测试实例
从持续集成到部署
Jenkins最基本的持续集成
在做好了各种测试实践后,需要考虑如何保证这种实践在每一次提交都产生效果,并且提供对代码库的不断保护。这里引入常用敏捷实践中持续集成的概念。
持续集成主要是为了能够更好为产品在整个开发过程中提供交付保障。相对比传统服务器领域,移动开发中,碰到最多的问题是环境的不同与测试包的相对封闭。以前
通过改一个配置实现服务器实时测试修改的方法,很难直接应用于移动开发。此外,作为整个服务边缘的移动应用,往往会遇到大量不同的测试环境。
所以如图6所描述的,需要在持续集成服务器上部署相关的应用包,完成对不同环境的开发与测试。这样的测试通常是由持续集成服务器引导的不同物理移动设备来完成,以保证最终测试包的可交付性。尤其适用于对于网络和性能等相关的测试。
图6 利用相关应用包完成对不同环境的开发与测试
当然,如果细心的话,也可以看出图6中对于产品版本与配置管理的一些端倪。
OTA自动化部署
持续集成帮助我们有了稳定的应用包。有了这些包,我们现在就差最后的一步—自动化部署。在移动的真实交付中,由于安装包需要下载到移动终端完成。所以自动化部署也必须要模拟这一过程完成交付。
这里我们推荐使用持续集成服务器关于邮件服务推送的应用插件来完成。具体的过程通常为,在完成编译并顺利通过测试后,持续集成服务器会根据制定终端列表,推送测试内容以供安装。推送者收到请求后,可以根据需要自行安装相应apk文件,达到部署效果。
这样做打通了移动开发最后一步的障碍。并有效沟通了管理、设计、业务与测试的各个部门人员。持续集成管理员还可以通过管理安装邮件列表的形式,自行选择不同版本,环境安装包对不同相关人员的推送。
让调试可持续化
有了开发,有了一系列持续化集成作业,有了作业完成的自动化打包部署,貌似可以休息了。但正如DevOps概念所说,虽然做了许多努力,但我们依然承认会有
错误的出现,那么如何尽早发现错误并准确定位,最终完成就显得尤为重要。在不同阶段,我们尝试使用不同的方法来进行调试。下面介绍我们的两个实践方法。
开发后调试。旧有的logcat是一个很好的调试工具,它在开发中起到了重要的作用,但对于做探索测试的QA们而言,大量繁琐的Debug信息往往令人看得眼花缭乱。
这 时我们就需要一个更好的工具进行错误栈的准确定位,因此我们引入了Acra。这是一个开源工具包,其功能是自动化地将Android应用的错误报告发送到
GoogleDoc或着Email等的可记录文本中。它的目的是为了帮助Android应用开发者在错误发生时收集行为和技术数据。
上线后的持续关注当然在上线后,Google Play也为我们提供了一个非常有好的平台用以收集各种来自用户的反馈信息。其中包括下载量、评价、应用异常报告等。
图7 移动循环开发流程
在 应用异常报告中,详细记录了整个错误堆栈的信息,更包括错误信息的总数量、周发生次数,以及版本时间等信息。因为Android平台设备性能、具体型号等
多方面内容,并不一定所有的错误信息都一定去进行处理,但这个平台提供了一个持续关注上线情况的基础,为产品的紧急处理、故障修复和在开发提供了重要的信
息来源。
这里还要强调,测试优先的TDD实践,在每一个错误修复前,都应该把测试补好,这是同样错误不会再次出现的最重要保障。
总结
本文提出了移动开发过程中常见的问题,并分析了基于开发与维护过程中的一些具体实践方法,尝试从移动应用的开发技术角度提供解决方案。
除此之外,事实上和常见的软件开发一样,还有诸如配置管理、发布管理等也是必不可少的。我们总结了如图7所示的一个移动开发循环。通过对不同方面的配置技术实现,完成移动的全流程开发,供读者做更深的思考和方法演进。 |