Visual Studio Team Architect团队的敏捷开发 (第三部分)
我在这个敏捷软件开发系列的上一篇文章中讲述了我们团队计划sprint的过程。在这篇文章中,我将会进入执行环节,详述我们如何进行一个具体sprint的实施。
在开始之前,首先来回顾一下我们是如何得到在sprint中需要实现的用户故事(User Story)列表的:首先,团队会根据开发团队在以往sprint的经验中得出的团队开发速度评估,以及对产品待开发事项(Product Backlog)的粗略的成本评估。基于这两个评估,开发团队从产品待开发事项中挑选出一个用户故事的候选列表,提交给产品利益相关者(Stakeholder)进行讨论。在讨论的过程中,伴随着用户需求的进一步明确与细化,该列表的优先级可能会有相应的调整。回顾这个过程,不正是敏捷开发过程与传统瀑布式开发流程最大的不同所在吗?
在上一篇文章中,我介绍了开发流程中团队里的三个核心角色(项目管理,开发,测试)如何协作定义用户故事。这种协作机制是非常重要的,因为它不但能够使团队中所有的参与人员对产品的设计有一个统一的认识,同时它还帮助团队在进入编码阶段前就找到产品中一些可能的漏洞。
当然,这种团队内部的沟通协作机制也可以扩展到实现设计以及测试计划阶段。正如我之前提到的那样,在我看来,敏捷开发并不意味着不需要架构设计和文档。对于任何一个有一定复杂度的项目,尤其是对于那些开发团队分布在不同地方(就像我们Visual Studio Team Architect团队一样)的项目而言,编写、评审设计文档对于实现那些复杂的功能是十分关键的。我们团队对此有两种处理方式。一种方式是:如果功能不是非常复杂,并且各方能很好地理解设计、设计本身不存在重大的不确定因素时,我们就会召集一个简短的评审会议,负责该功能的开发工程师会准备一些简单的设计文档(比如类图和顺序图),在会上给团队里的其他人员讲解他/她的整个设计思路。团队成员会一起评审该设计,并且对开发工程师可能忽视的地方提出意见。测试工程师则可以通过这个过程了解开发工程师的底层实现思路,从而他们能够在自己的测试计划中添加所需的白盒测试。同时,这个评审的过程也给开发工程师和测试工程师提供了一个机会,使得他们能够更清楚的划分单元测试、验收测试以及功能测试的范围,从而更好地各司其职。
[注意:这是一个有争议的观点,甚至可作为一个辩论的话题。对此我个人的观点是:多余的测试对于项目组中珍贵的资源(比如工程团队和机器)来说是一种浪费。我认为既然测试工程师的最终目标是保证产品的质量,那么为什么他们不能利用开发工程师编写的单元测试作为产品质量验证的一部分呢?]
另一种方式是:当功能非常复杂或者大家对设计内容理解不充分时,我们会尝试针对这个功能做一个“试探”(Spike)(即构建一个原型系统)并将发现记录下来。我们团队,以及一些经过我的推荐使用这个实践的团队都发现对于具体的功能实现来说,“试探”无疑是一个非常有用的方法。下面我给大家展示一下“试探”结果的文档模版:
标题
- 目标
<对本次“试探”给出一个总体的目标,例如:这是为了让项目经理更好地定义用户的需求,还是为了让开发工程师更好的理清技术难题,评估可能的解决方案,等等。>
<有时这一部分也可以包括对“非目标” 的描述—— 在这次“试探”中我们不会试图解决的问题>
- 场景
<对用户的使用场景进行描述>
- 解决办法
<对本次“试探”所要解决的问题提出一个解决方案概览>
- 细节
<对解决方案进行详细描述>
- 已知问题
<对在本次“试探”中所明确的待解决问题进行罗列>
- 结论
<根据本次“试探”的结果对问题推荐一个解决方案>
准备好这样一个模板,在遇到棘手的问题时采用“试探”并根据此模板填充必要的细节,能很好地协助我们选择较优的设计方案,更重要的是能更有针对性地解决在“试探”过程中发现的问题。如果我们决定要做一次“试探”,那么这将被作为一个单独的任务(不计算在团队承诺要完成的故事的范畴内),同时要为此安排适当的资源。
现在让我们来看一下测试工程师的工作。每个用户故事都会有一个测试工程师负责其质量,他/她会为故事设计两个测试计划:一个是“验收测试计划”,另一个是“功能测试计划”。验收测试是黑盒测试,其目的在于验证用户故事是否按照设计预想的那样被实现。这里需要注意的是,在着手实现一个用户故事之前,准备好这样的验收测试步骤(当然,这样的验收测试不一定全部是自动化的)并且将其集成到用户故事文档中去是一个必要的步骤。验收测试的编写并且通过,需要被纳入用户故事“完成”的标准中去。如果没有经过这样的一个步骤,用户故事就不能被签字认可。相对而言,功能测试计划是一个更为详细的计划,测试工程师需要针对不同的代码路径以及不同用户输入情况进行测试,从而保证软件在各种情况下都能正常工作。需要强调的是,测试工程师所编写的测试计划也需要通过开发工程师的评审(好吧,至少大多数情况下如此)。
在进入签入流程之前,我想先简要介绍一下我们的源代码管理系统以及我们的源代码分支(Branch)结构。作为Visual Studio Team System开发团队的一部分,我们自然采用Team `Foundation Server作为我们的源代码管理系统。我们采用多层的源代码分支结构来管理我们各个功能团队的开发。下面的这幅图展示了我们的源代码分支的层次结构。
主干分支(Main Branch)是要发布给客户的所有产品功能集成、构建以及测试的地方。产品单元分支(Team Branch,也可称为Product Unit Branch)是所有属于同一个产品单元的相关功能集成、构建以及测试的地方。而具体的产品功能开发则是在功能分支(Feature Branch)上完成的,在大多数情况下(如图所示),在源代码层次结构中,功能分支一般处于主干分支之下两层的位置。
源代码分支结构的核心思想是让各功能开发团队工作在彼此独立的功能分支上,专注于自己的工作以保证该功能的质量,避免过早处理复杂的分支集成。更为重要的是,这样的结构有利于各功能开发团队根据自身情况选用合适的开发流程,只要保证他们开发的功能在与上一层分支集成时可以符合统一的质量要求就可以了。
现在让我们回到签入流程,下面的流程概括描述了一个开发工程师在签入代码前所需要完成的各个步骤。乍看起来有很多的工作需要做,但是根据我的经验,在代码签入前开发工程师投入一点时间在这些步骤上,将会为将来整个产品开发周期节省大量的时间。
尽管所有的这些步骤都很重要,但是我还是想重点强调以下几个步骤:“编写单元测试”(Unit Tests),“代码审阅”(Code Review)和“代码分析”(Code Analysis)。几乎所有的新增或修改的代码在签入之前都需要有相应的单元测试代码来保证它们的质量。在签入代码的同时将相应的单元测试代码一并签入带来的好处无需多言,仅此一条就足够说明问题了:如果能够通过单元测试获得较高的自动化测试覆盖率,就能很好地避免在将来的产品开发周期中随着产品功能的增加、代码重构的发生而引入回归缺陷(regressions)。在日常工作中,我们使用Visual Studio开发工具集成环境中所提供的单元测试框架,这样开发工程师可以在签入代码之前在集成开发环境中很方便地编写和运行他们的单元测试以验证他们的代码。下面是一个有关我们日常工作中单元测试的一个截屏。
在我们团队里,在自己的代码被团队其他人员审阅之前,没有人会直接将自己的代码签入。在实际工作中我们并没有用什么特殊的工具来进行代码审阅,开发工程师或者测试工程师(是的,测试代码在签入之前也需要进行代码审阅)会在Team Foundation Server中建立一个“Shelveset”,并且写邮件告知自己的开发小组。小组的其他成员会审阅这个“shelveset”中的代码并且给出意见。经过必要的讨论以及更新后,shelveset中的代码才会最终被签入。这样的一个代码审阅过程不但能够在代码签入之前及时发现并修复错误,而且它还为团队成员之间互相学习的提供了一个绝好的机会。
另外一个在代码签入之前非常重要的工作就是运行代码分析工具对代码进行分析。我们会把分析工具所得到的每一个警告都当做构建错误来认真对待。当然,有时候我们也会根据实际情况忽略某些警告,但是这必须是在对警告进行了充分分析并且提供恰当理由的情况下才会发生。VSTS代码分析工具另一个很酷的功能是我们可以根据自定义的规则对代码进行分析,将其中的可疑部分标记为错误。相对于运行代码分析工具和分析所产生的警告所消耗的额外时间成本而言,它为产品质量的提升所带来的回报是巨大的!
一旦代码被签入,将会在团队的持续集成服务器(Continuous Integration Server)上触发一个新的构建过程并且运行相应的测试。我们根据目的的不同设置不同的服务器。首先,我们会建立一个团队构建(Team Build)服务器专门用于构建验证测试。这样做的目的是为了找出那些通过前面介绍的签入流程而没有发现的构建错误。[注意:在我们当前的这种“乐观的”签入系统下,有时由于不同的开发工程师的独立签入操作所带来的冲突是不可避免的。这种情况可以通过“悲观的”签入系统(即将所有的签入操作都通过专门的签入服务器来排队进而提交)来缓解。但根据我们的经验,目前已有的“悲观签入系统”的流量一般都不是很大,因而往往成为整个开发流程的瓶颈。]下面的截屏展示了经过一系列单独的签入操作以后所产生的构建结果。
[译者注:所谓的“乐观的”签入系统是指系统假定认为开发或测试工程师在签入代码之前已 完成了必要的验证工作,系统本身不会做更多验证。]
我们还有一个“滚动构建(Rolling Build)”服务器,其任务是不断同步代码,构建以及运行一组特定的快速测试集。该测试集具备足够的测试覆盖率以保证本次签入的代码不会对其他的功能造成回归缺陷。我们的目标是争取让滚动构建中的每一轮能够在60到75分钟之内完成,这样我们可以在第一时间得到有关这次构建的结果。在实际情况中,构建花费的时间会稍微长一些,大概在90到105分钟左右。事实上,在刚开始运行这种滚动构建时,花费的时间比这个还要更长一些。不过,通过我们的团队成员不懈努力的修改构建验证测试的代码,在不影响测试覆盖率的情况下大大缩短滚动构建的时间。当然,在这个过程中我们也总结并应用了很多技术(例如“Humble Dialog Pattern”)来使得测试能够运行得更加快捷、稳定(从而避免了重复运行)。同时,我们也通过测试适配器(Test Adapter)技术生成了一个利用Visual Studio的集成开发环境作为宿主环境的适配器。这样做的好处在于我们可以直接在同一进程内对用户界面上的组件进行测试,而不用跨越进程对界面控件进行访问或发送“Send Key”信息。如果大家对测试适配器技术感兴趣的话,我可以在以后的文章里与大家更深入地探讨这个话题。
另外,我们还有一种每夜(或闲时)运行的构建机制,将运行我们所有的自动化测试。这样一次构建大概需要5到6个小时才能完成,构建的结果会在第二天进行分析并且将那些在构建过程中发现的非核心场景的回归缺陷进行修复。我们发现,使用这样一组混和的构建机制能够最大限度地减少我们的回归错误。更为重要的是,它使得我们在对代码进行大的重构或者将新开发的功能集成到已有功能中去时更有信心。
在构建中所执行的测试集包括验收测试(这里又包括性能测试和压力测试)、功能测试、集成测试以及整体场景测试。因为所有的这些测试都使用一个共同的测试框架(基于Visual Studio中所提供的测试框架)的,所以任何一个团队成员都可以根据需要运行任意一部分测试,而无需繁复的步骤来设置测试环境。下面的截图展示了我们测试项目的组织结构。
一般来说,每一个sprint的最后几天往往是留给剩余测试的编写、运行以及验证的(这是因为功能测试的进度往往会比用户故事的完成进度落后个几天)。而这段时间往往也是进行缺陷大扫除(Bug Bash,所有团队成员集中使用产品以寻找缺陷)以及使用探索性测试来寻找产品潜在的重大问题的时间。理想状态下,我们会尽可能在sprint完成之前修正所有的缺陷,但实际情况中,我们往往会将一些缺陷的修正时间延后,以便我们能够按时开始下一个sprint去完成下一组用户故事为客户提供更大的产品价值(当然,这些需要得到利益相关者的同意)。
每一个sprint都会以一个团队总结会议来作为结束。在这个会议上,我们会将新完成的用户故事(功能)向包括利益相关者在内的更大范围的团队成员进行演示。任何的反馈或者修改意见都会被作为产品待开发事项记录下来并定好优先级。同时在功能开发团队内部我们也会对sprint进行回顾和总结:哪些我们做得好的,哪些是我们需要改进的;我们会从那些被公认为需要改进的地方中挑选出几个“待办事项”记录下来,作为小组下一个sprint要加强的重点。当然,待提高的地方并不总会在下一个sprint中就得到改进,我也常常对未能做好这些事项的跟进工作而感到不好意思。这是我在接下来的工作中需要努力的方向(我已经把这一点写入我在下一个sprint中需要做的待办事项了)。
Ramesh Rajagopal
Visual Studio Team Architect 中国团队经理
译/钟鸣、林俊彦、陆榕
注:
- 本文已被收录于《程序员》2010年1月刊;
- Visual Studio Team Architect团队的敏捷开发 (第一部分)
- Visual Studio Team Architect团队的敏捷开发 (第二部分)
广告:京沪两地敏捷Scrum实战营(免费)
InfoQ中文站、雅各布森中国有限公司和微软中国有限公司联合举办。
上海专场:2010年01月23日(周六)9:00 ~ 17:00
北京专场:2010年01月30日(周六)9:00 ~ 17:00
报名详情点击这里