专业的咨询师不仅需要对新兴的技术和趋势有足够的好奇心去探索,也要能够对过气的技术工具有自己的评价,这样当客户咨询师为什么做这样的技术抉择,而必须放弃手头已经的技术投资时,才能有足够的说服力。
这次要介绍的是2013年12月版ThoughtWorks Tech Radar中的Heavyweight Test Tool条目,状态是Hold。Hold就是建议停止使用。
专业的咨询师不仅需要对新兴的技术和趋势有足够的好奇心去探索,也要能够对过气的技术工具有自己的评价,这样当客户咨询师为什么做这样的技术抉择,而必须放弃手头已经的技术投资时,才能有足够的说服力。
这次要介绍的是2013年12月版ThoughtWorks Tech Radar中的Heavyweight Test Tool条目,状态是Hold。Hold就是建议停止使用。
个人的观点,持续集成是敏捷软件开发中最为重要的实践,没有之一。而加速反馈的重要方式,就是持续集成结果的可视化呈现,也即信息辐射器,作为团队监控软件构建状态的接口。专业的持续集成平台,不管是像ThoughtWorks的商业产品Go,还是CI开源世界代言人Jenkins,都提供了开箱即用的CI Monitor。
下面是Go的CI Monitor界面:
在Scrum中,整个团队会举行计划会议,以决定接下来的这个Sprint会做哪些事情,决定由PO和整个团队来做,依据就是优先级。PO会去选择那些从他的角度看优先级最高的,在上个Sprint已经完成工作的基础上,从Product Backlog中新添加那些对于软件用户来说最具优先级的特性,来交给团队实现。这里当然会有特别的情况,比如会有新添加的特性需求,也会有上个Sprint未完成的功能,还会有个别需要去掉的需求,需要PO在这里统筹安排,并和团队一起协商,达到一个最优化的选择和决定。
然而这里说的不是泛泛的迭代计划该怎么做,而是第一个迭代的计划该怎么做,根据什么依据或者原则来决定第一个迭代做哪些故事,我们会看到这里需要有更多的考量因素。
每一个迭代的计划过程,都是根据团队的实际情况,根据一些需求和因素来选择合适的故事进入开发流程。但对于第一个迭代来说,需要的考虑的因素稍微多一点。如上所述,每个因素间根据团队自己的特点和实际情况,会有所侧重,但结果期望会是一个相对正确的选择。
在敏捷开发中,故事墙作为information radiator,在支撑整个软件团队的日常开发,可视化开发进度和开发中出现的问题,起到非常重要的作用。团队的开发状态和进度,在任何时刻,你只需要抬一抬头,就可以一目了然:当前谁在开发什么任务,是否有闲置的资源,开发和测试的工作分配是否合理,是否有过量的Bug等待修复,不一而足。
虚拟的电子故事墙也应运而生,比如Mingle和TFS,还有一些开源的产品经过改造后,也可以极度模拟实际的物理墙界面,并更加容易创建和编辑并删除。尤其在保存开发过程历史方面,比起物理墙有着得天独厚的好处。物理墙基本上只关注当前迭代的进程,在每次进入新的迭代,物理墙更新,上一迭代的故事卡片被回收或者遗弃,难以定位和回顾。虚拟墙因为信息化的本质,更容易搜索和存档,保留快照。此外,电子故事墙在统计和制作图表方面的功能,更是物理墙无法企及的。电子墙可以随着故事卡的挪动,动态算出当前状态下的各种数据,并渲染对应的实时更新的图表,为团队和客户提供数据和可视化方面的决策支持。更重要的,虚拟墙天生支持分布式开发,因为地区甚至时区有差异的团队,如果需要共用一个故事墙时,唯一的选择只能是虚拟墙,每一方团队都能看到时刻最新的故事墙状态。
如何解决好两者之间的关系,在ThoughtWorks内部以及客户的现场,我们都遇到了类似的问题。一般来说,团队自身或者开发者们更喜欢物理墙多一点,感受直观,把注意力和时间都聚焦在故事卡对应的任务本身,而不是被动地吸引在虚拟墙软件自身以及如何学习使用并挪动虚拟墙上的电子卡上(你会看见在站会时,每一位开发人员在挪动电子卡时会有多么奇怪而有趣的事情发生),对他们是更重要的事情。而对于团队的PM或者stakeholder们来说,虚拟墙可以远程打开,开发状态一目了然,更不用提统计和图表所提供的支持。
很多团队会选择两者都采用,但不得不问到以谁为主以及如何同步的问题。以谁为主还是比较好解决,物理墙面向开发团队,虚拟墙面向管理者和stakeholder们。所以最大的问题在于如何保持两者之间的数据同步。说白了就是,物理墙上的故事卡挪动,要及时在虚拟墙上反映出来,尤其虚拟墙软件对于故事卡的每一步挪动都会记录相应的数据变化,从而为精细的图表和统计提供数据,这样物理墙和虚拟墙之间的故事卡数据同步显得尤为重要。但现实情况和经验表明,数据同步做得很不如人意,开发者的关注点在物理墙,往往容易忘记及时更新虚拟墙上的故事卡,他们甚至对于这样一件重复的事情感到愤怒。而如果由PM每天下班前对两个故事墙做同步,又显得无聊之极。
怎么办?我更相信鱼和熊掌不可兼得,对于一个团队,如果既要享受物理墙带来的可视化和信息辐射的好处,又需要虚拟墙带来的强大的编辑、统计以及图表功能,就不得不去承担因为同步二者的故事卡数据所带来的辛苦甚至是痛苦。选择团队的开发人员来各自负责同步两个墙之间的数据,还是由PM每天找一个正确的时间来同步,都是由团队自己来决定,至少我们是这么做的。你还有其他的办法吗?
敏捷软件开发与其他软件开发方法学最大的区别,在于敏捷是承认并拥抱变化的。为了这样的变化,敏捷的不同方法,比如极限编程、Scrum引入不同的技术实践和流程,像持续集成、测试驱动开发以及短迭代周期等,来确保即使在需求的快速变化下,也能保证交付的软件总是满足用户的需求,是高质量的价值交付。
从敏捷软件开发宣言来看,并没有涉及测试的内容,更不用提为QA即测试人员提供指导性的建议。就是以极限编程的14个推荐实践来看,表面上对于测试的提及也只是验收测试,甚至没有敏捷测试这个概念。这样让很多人一度认为敏捷软件开发是不是之以程序开发为导向的,是不是我们测试人员能从敏捷获得的直接支持少之又少,我们是不是被遗忘的一个种群?
答案必然是否定的。还以极限编程为例,除了验收测试以外,极限编程提及的完整团队、用户故事、短交付周期、持续集成等实践,都在从不同的维度对于测试工作的流程和方式甚至对它思考的角度提出了变化的要求。
完整团队从文化氛围和组织结构上明显区别与过往的测试人员参与感,测试人员不再是对软件系统质量负责的唯一角色,对质量负责的是全体团队成员的职责。用户故事改变以前对于软件系统功能和模块的划分,而是从交付的独立价值出发,改变了测试人员对于测试案例准备和验收的方法。短交付周期,无论是两周还是四周,都给整个团队带来了巨大的变化,开发和测试不再是独立而又顺序的过程,开发和测试互相穿插,成为一个快速反馈的过程。持续集成是软件系统开发过程的晴雨表,其中价值相当大的自动化测试仍然和我们的测试工作脱不了干系。
可见,敏捷和它的方法,虽然没有显式地给测试工作以指导建议,但隐式地要求了我们测试人员仔细思考测试本身在敏捷项目中所需要发生的变化,我们测试人员的职责和工作范畴发生了哪些变化。
与敏捷开发一样,敏捷测试针对不同的项目上下文和不同的团队组成和背景,有不同的适配模式。跟敏捷软件开发的宣言类似,敏捷测试也有一系列可以恪守的原则。经过不断实践和经验,ThoughtWorks的同事同样提出了《敏捷测试宣言》:
第一点与完整团队有关。虽然独立的测试团队可以从外部视角观察软件质量,但真正的软件质量来自测试人员属于一部分的完整团队,不再区分彼此的开发团队和测试团队,不再有彼此分离的目标。整个团队为软件质量和客户价值共同负责。
第二点针对性自动化测试胜过广泛的回归测试。随着软件系统开发的进展,后期引入的新功能和缺陷都会带来大量和重复的回归测试,自动化测试是代替人工繁琐而无聊的回归测试的唯一办法。
第三点提到的如何对待缺陷恐怕是时下各个测试团队最为纠结的内容了。预防缺陷胜过报告缺陷,预防缺陷才是测试工作最大的价值所在。敏捷测试会尽早介入软件系统的开发过程,和业务分析师、客户分析需求和价值所在,以用户故事和验收条件来驱动开发,以短周期迭代和持续集成为反馈,可以尽早发现存在的缺陷,从而极大降低后期才报告以及修复缺陷所带来的高额成本。在我的团队,测试人员甚至可以和开发人员结对,共同确认缺陷根源,修复缺陷,并添加自动化测试确保缺陷不会再次发生。
第四点的探索性测试的重要性没有人会怀疑,测试人员可以凭借自己的经验积极、自由地发现质量问题,而不仅仅是反复运行已经定义好的测试。这样可以证明软件不仅仅做了它该做的事情,还证明软件没有做它不该做的事情。
1. 反映真实需求
这里存在先写测试和后写测试的区别。
先说后写测试。根据很多经验,在直接写产品实现代码时,需要考虑需求,同时需要兼顾实现的细节,用什么算法和语法。在对需求和考虑和实现细节间来回,很容易让人产生对其中一方的疏忽,遗漏掉一些需求方面,甚至在实现上存在缺陷。
有人会说我可以通过后写测试来保证。第一经验是,很多人都不会在实现完成后,补充测试,因为还有更多的工作和需求需要实现。第二是开发人员很容易在后补的测试中,只是试图去测试他已有的实现,而不是需求本身,很容易遗漏掉一些边界检查之类,在测试时,已有的实现细节会在脑子里面先入为主,即使实现存在问题或有漏洞,也很难在后补的测试中测出来。我阅读过很多面试者的测试代码,很明显都是后补的,因为一些很明显的问题没有测出来,甚至已经实现的逻辑也是只测试了一部分。而这些面试者都承认。
结果就是,一旦代码签入,开始集成之后,暴露出问题,后补的测试起不到对于实现代码的保障需求的作用,开发者不能不借助调试工具,单步跟踪实现代码的每一行去寻找问题发生的原因。
再说先写测试。按照TDD的流程,先写失败的测试,再写恰好让测试成功的实现代码,最后重构,如此往复。这样的好处在于,每先写一个测试,都是在试图用自己对问题和需求的理解,来定义实现代码的架子。从测试的不同角度,范围、边界、大小、功能等等,来定义实现代码将来会是个什么样子。一个测试接着一个测试,利用TDD的过程,把实现代码恰好不多不少,正好驱动出解决这个需求和问题的实现代码来。
这里的重点在于,先写测试可以让开发者把重点放在理解需求和实现需求上,而不是一开始就陷入实现的细节中同时兼顾需求,掉入两者都兼顾不好的境地。先写的测试代码,作为副产品,可以作为验证需求的得力保障。
2. 设计在其中
先写测试对于设计的好处在于,先写测试先定义新的类,以及定义类与类之间的关系,就是在定义类与类之间如何交互,每个类如何暴露自己的接口,类和类之间的引用关系。这时,测试代码会逼迫开发者认真考虑如何分解类与类之间的耦合关系,这样产生的实现代码更容易利用了IoC和DIP的模式,实现面向接口编程。
这样实现代码的好处还在于,代码的可测试性很高,在加入更多的测试代码和新类的时候,同样借力于已有类的面向接口和依赖反转所带来的可测试性,达到新实现代码的面向接口和可测试性,这样进入良性循环。而这对于整体的代码和设计,获益良多。换句话说,测试即设计。
回头看后写测试的情况,因为从一开始开发者把重心放在实现的细节和功能需求的往复上,对于代码设计、类的关系和定义很容易疏于考虑,产生的结果可能是耦合紧,可测试性差。
3. 增强信心
在我看来,软件开发周期、软件交付最大的问题在于交付后的运行和维护阶段,正是这个阶段才是软件在持续交付价值的时期。在软件的可维护性,在这个阶段凸显价值。在软件交付后,包括软件开发周期期间,不可避免的就是开发者在根据新的需求,逐渐添加新的功能代码,或者修复一些已知的缺陷。
很多经验表明,在开发者按照需求添加一些新的代码进入系统,或者试图修复已有缺陷时,很容易导致既有功能出错,也就是新引入的代码打破了既有代码的逻辑,导致回归问题的出现。因为软件系统的可测试性差,无法做到快速频繁自动的回归测试,带来的可维护性自然也很差。
而作为TDD的副产品之一——可以快速频繁自动运行的测试代码,可以在开发者新引入代码之际,给予开发者足够的信心,每次添加一点新代码,一个方法,一个类,都已频繁运行已有相关的测试代码,来确保新引入代码不会打破已有的功能。
在持续集成中,这些测试代码可以帮助验证每次签入的代码都不会破坏掉已有的功能。这也是《重构》里面反复提到的在每个重构小步骤后都要运行所有的测试代码的原因所在。
4. 粒度和进度
按照TDD的原则,先写测试,可以让开发者在同一时间只关注在功能需求的一小部分,把功能需求且分到一定小的粒度,用测试代码去表示这样的需求,用实现代码让测试通过,实现这样的需求,然后重构。
这样的好处在于,开发者自己的注意力和重心不用在整个功能需求内的小需求点之间犹豫,每次注重解决单个小问题,解决完一个进入下一个小问题的解决。在保证小粒度实现的同时,保证进度可以随时被打断,但同时被打断时已经做完的是完整可以运行,至少是实现了部分需求的实现代码。
而后写测试的代价是,首先实现代码很有可能包含设计上的问题,甚至含有缺陷,在完美的测试代码完成之前(事实上这是不可能的),可以交付的是可能存在严重缺陷,甚至是曲解了功能需求的实现代码。
上周四的中午饭后,我们team坐下来完成了一次Code Kata练习。跟之前的几次邀请PwC team同事过来pair和coach不同,这次是我们组自己内部完成一次练习,题目来自Yuheng之前的一个练习:文曲星上的猜数字游戏。
题目规则很简单:
开始提出的要求简单:用TDD驱动出实际代码。
大家抽签后开始pair,Yuheng还会一边计时一边催促,pair的气氛基本保持紧张有序。
在我和XuChen pair开始代码后,我们很容易写出第一个测试1,期望返回结果是”4A0B”,最简单的代码实现让测试变绿。
当我们试图写下第二个测试2,试图期望返回结果是“2A1B”时,我们发现不得不面对如何这个游戏规则的最核心算法,但可惜的是,猛然间对这个算法没有头绪。
局面一时僵住,停顿了有两分钟。
直觉和一点经验告诉我们,这时该拆解任务了。很容易想到,如果计算出几个A和几个B是很直白的拆分。
我们可以很容易写出一个测试3,测试猜测的四个数字跟事先给定的四个数字相比,有几个A类数字(循环比较),我们称之为perfect number。
也很容易写出另外一个测试4,测试猜测的四个数字跟事先给定的四个数字相比,有几个B类数字(在上个算法的基础上再循环比较),我们称之为good number。
写完针对测试3和4的实现代码,我们发现我们已经完成了最基本的算法,功能完成了。剩下的是重构代码。
从我们这次的经验看,我有一些收获和心得:
我们在实现代码时,仍然欠缺的是: