在上一篇文章中我们花比较大的篇幅介绍了敏捷业务实践中的计划游戏,在这篇文章中我们将介绍介绍生命之环中外围剩下的三个业务实践。
小步发布
小步发布这一业务实践建议开发团队应该尽可能频繁地发布软件。在敏捷早期,这个时间节点大概是“一到两个月”。现如今,发布周期的目标是无限短,即持续交付:每次代码更改后就将代码发布到生产环境中。
虽然缩短的是发布周期,但其实小步发布这个实践的真正目的在于缩短所有周期。但是由于源代码管理的历史原因,缩短周期这件事情从根本上就会面临很大的历史惯性。这一切要从软件行业早期开始讲起。
纸带打孔时期
在软件行业刚刚兴起的时候,所有的”代码“是储存在打孔纸卡上的。一张纸带可以容纳 80 个字符,代表一行程序。程序本身由这样一张张的纸卡组成,最终整个程序将是一大箱成捆的纸卡。
这些纸卡被存放在柜子里,如果有人想修改代码,这需要签出源代码,在获得所有者的许可后,便可以从柜子里清点出要修改的那部分代码 - 这是真正的签出源代码。
这时由于你从物理角度获得了源代码,意味着只有你有资格修改这部分代码,其他人都碰不到它。只有等你完成了修改,将源代码存回柜子里,其他人才有机会修改。
这个时间周期不固定,可能是几天、几周甚至是几个月。
磁带时期
后来随着科技的发展,代码开始存储在磁带上,除了容量更大,也更容易被复制了。此时修改代码变成了:
- 从母带架上取出母带。
- 将要编辑的代码从母带上复制到你自己的工作磁带。
- 放回母带,其他人可以继续复制。
- 将彩色大头钉固定在你正在编辑的模块旁边的签出板上,一个人使用一种单独的颜色(然后人多就会用完所有颜色)。
- 在你自己的工作磁带上进行编辑、编译和测试。
- 再次取出母带。
- 将更改完成的模块从工作磁带赋值回母带中。
- 将新的母带放到母带架上。
- 从签出板上取回你的大头钉。
这时的好处是你不再在物理上拥有源代码,这代表着别人可以同时修改不同的模块,但这也就意味着别人可以不遵守约定,偷偷修改你正在修改的模块。
这时的工作周期有一定的缩短,变成了几个小时、几天或者是几周。
磁盘和源代码管理系统的出现
再后来源代码的存储介质变成了磁盘,但是仍然使用大头钉的方式来标记谁在修改哪一模块的代码。然后真正的源代码管理工具出现了。
早期的其中一个源代码管理系统 Source Code Control System(SCCS)。SCCS 的行为与签出板一样,将模块以悲观锁(Pessimistic Lock)的方式锁定在磁盘上,防止别人编辑。
后来 SCCS 被 版本控制系统 RCS(Revision Control System) 取代,然后 RCS 又被并发版本系统 CVS(Concurrent Version System) 所取代。这些工具在进行微小改良的同时本质都没有改变 - 都是使用悲观锁来控制相应模块不发生冲突。
虽然此时的周期还是不短,但是磁盘允许我们极大地缩小模块的规模。模块的小型化有效地缩短了周期时间,因为模块越小,保持签出状态进行修改完成的时间就越短。
Subversion
Subversion(SVN) 与上述工具不同,其提供了乐观锁。这使得多个开发人员可以同时签出一个模块。SVN 工具会对此进行追踪,并自动将多人的更改合并到模块中。如果有冲突,则要求先解决冲突才允许签入代码。
这样的流程会大大缩短周期时间,使其缩短至编辑、编译和测试一系列小更改所需的时间。此时签出时间不在是制约周期时间的因素,主要因素变成了修改紧耦合代码所产生的冲突。
Git
如今我们使用 Git,此时已经没有签出时间这个概念了(当然,除了那些超大型的代码库)。程序员可以在任何时间提交对模块的任何修改。如果这些模块出现了冲突,程序员可以在任何时候来解决冲突。
充分解耦的小模块和快速的小步提交共同作用,使得周期时间可以缩短至几分钟。在此基础上覆盖以全面、运行快捷、几乎可以测试任何功能的自动化套件,你就具备了持续交付(Continuous Delivery, CD)的条件。
小步发布的目的不仅仅是为了缩短发布周期。为了实现缩短发布周期这个目的,大家就必须使自己的代码充分解耦,同时采用小步快速提交的策略,将自己的修改快速提交到代码库。而为了使得自己的小步修改是可工作的,又要求有一系列的测试套件来保证代码质量。
而为了缩短发布周期,组织需要打破发布和部署之间的耦合。最终,“是否要部署”只是一个业务决策,每一次决定部署,都已经变成了一次发布。
验收测试
最令人困惑的实践
验收测试这一敏捷实践的基本思想是:应该由业务方负责说明需求的规格。而问题恰恰出在规格说明上。
许多业务方认为这个词的意思是:我只需要随便比划两下,用几句含糊不清的话来描述我的需求,开发人员就应该自己能够找到所有的细节。而程序员们又希望业务方能够精确定义系统应该做什么,最好能够描述清楚每一个细节。
而我们所需要的其实是介于这两个极端之间的关系。规格说明从某种意义上来讲,本质是一种测试,比如:
当用户输入有效的用户名和密码,点击登录按钮,系统将跳转到欢迎界面。
这就是一个标准的规格说明,但仔细想想,这也是一个标准的测试用例,并且这个测试用例是可以被自动化的。
其实这个实践指的是:只要可行,系统的需求就应该写成自动化测试。虽然看起来简单,但这个实践是所有敏捷实践中最不被理解、使用最少,也最混乱的实践。下面这几个问题将为你揭晓为什么会有这样的情况:
首先是,谁来写这些测试呢?如果按照本节第一句话描述的基本思想来说,既然规格说明是业务方提出来的,那么是不是应该由业务方来写这些测试呢?
下一个问题是,大部分业务方其实并不具备编写代码的能力,编写代码不应该是程序员的工作吗?
但是如果是程序员来写这些测试的话,他们会以以技术的思维来写,并不会以业务的思维来写。这意味着这些测试除了他们自己谁也看不懂,因为里面充满了技术细节。所以这个问题还是推回给了业务方,应该由他们来编写。
但是如果由业务方来编写,他们可能采用与团队完全不相符的技术栈,这意味着我们得重写这些测试以符合我们的技术栈。
尝试解决
实践混乱,就会催生出一系列尝试解决混乱的工具和方法论。FitNesse、JBehave、SpecFlow 和 Cucumber 等工具就是程序员们开发出来为了解决这些问题的工具。它们尝试将这些自动化测试的技术面和业务面分开。由业务方编写测试的业务端,程序员编写粘合代码将这些测试和被测系统绑定起来。
虽然梦想是美好的,但是业务方很是不愿意参与进来,比起形式化的语言,他们更希望用人类的语言,来编写这些规格说明。
于是程序员又做了一步妥协,他们编写测试但希望业务方至少能够去阅读代码形式化的文档,并验证测试是否正确,但业务方更想吧这样的工作交给 QA。
行为驱动开发
在千禧年之后,丹·诺斯(Dan North) 尝试重新定义 TDD,他称之为 BDD(Behavior-Driven Development, 行为驱动开发)。他的目标是从测试中去掉技术术语,使测试让业务方看起来更讨喜。
BDD 的支持者建议,业务方可以不写测试,但是希望他们能够以一种形式化的、基于场景的语言(比如 Given-When-Then)来详细描述他们的系统,这样的语言对程序员编写测试也有巨大的价值。
合作与实践
既然大家都不愿意一个人完成所有的事情,那如果大家一起来呢?实践的目的没有改变,还是只要可行,系统的需求就应该写成自动化测试。但是方式应该是业务方编写形式化的测试来描述每个用户故事的行为,开发人员将这些测试自动化。
回到最开始的问题,谁来写?答案是业务分析师(BA)和 QA,至于为什么,我们会在稍后解释。他们需要在迭代的前半部分之前完成测试的编写,然后程序员完成故事的开发并将这些测试集成到持续构建(CI)中。只有完成了测试的编写并且代码通过了所有测试,故事才算完成。
业务分析师和 QA
业务分析师负责澄清和理解需求方的需求,然后负责说明功能的乐观途径(happy path),因为他们需要作为中间人在需求方和程序员之间做大量沟通,没有精力描述所有悲观途径(unhappy path)。
而 QA 的工作是写出所有悲观途径,他们是既了解业务又拥有很强技术的人。他们能够站在业务和用户的角度找出如何破坏系统的方法,同时他们还能了解程序员的思路,戳穿他们的“偷工减料”。
QA
这个实践完全改变了传统 QA 的工作职责,他们不再作为测试人员在项目末期把关,而是在前期定义规格;他们也不再在项目晚期提出关于错漏的反馈,而是提早给开发团队提供输入,预防错漏的发生。
这意味着 QA 需要承担相对于传统项目更大的压力。为了确保产品的质量,QA 需要在每一个迭代的全期做不同的工作,而不是仅仅在结尾进行测试,他们是系统是否可部署的“批准者”。
这样做有什么好处呢?
减少遗漏测试的可能性
传统测试人员会在项目尾声才开始测试,这意味着他们会成为项目部署的瓶颈,为了部署,他们需要尽快完成测试工作。同时这样的工作方式会让上游所有的日期推迟都堆到测试身上,假如程序员的工作延迟了一周,测试的工作就就少了一周,因为管理者不会因为程序员的推迟交付多给测试时间,上线日期是几乎不会改变的。
那么在这样的情况下,QA 会如何测试呢?很简单,加速,不测所有功能,只测试新的需求或者那些改变了的功能,放弃测试没有改变的功能。于是遗漏开始了,全量回归被放弃,期许下次能有时间全量测试,可是下次往往意味着永不。
bug数即API
上述问题至少还有补救的办法,另一个将测试工作放在项目尾声的致命问题是,组织应该如何判断他们做好了自己的工作?很简单,发现的bug越多,他们做的就越好。以发现的bug数量来作为QA团队尽职尽责的证据。
于是,发现 bug 被认为是好事,且在一些偏激的组织内成为了 QA 团队的 kpi。
这样的问题或许在短期内不会致命,但是长此以往,组织内部一定会逐渐虚弱。
程序员
业务分析师和 QA 完成了测试的编写,但是他们并不运行这些测试。这个任务交给了程序员,程序员需要确保它们的代码通过所有的测试。此时形成了一种开发即测试的有趣局面。换句话说,程序员虽然在做开发的工作,其实他们也在做测试的工作,因为他们的代码通过了测试,这些刚实现的功能已经被测试过了。
这些测试会和开发过程中编写的单元测试一起被加入到持续构建中。每当一个程序员加入了新的模块,服务器会自动运行所有测试,而团队中的所有人都会关注这些运行,我们会在稍后的文章中有更详细的讨论。
完整团队
完整团队的事件最初被称为现场客户(On-Site Customer)。其理念是:用户和程序员之间的距离越短,交流就越好,开发就越快、越准确。
这里的客户是一个隐喻,指的是理解用户需求并与开发团队共同工作的某个人或团队。在 Scrum 里这个人或团队被称为 产品负责人(Product Owner, PO),他负责选择故事、设置优先级并及时提供反馈。
后来这个实践被改名为完整团队。这是为了强调一个团队里不是只有客户和程序员。开发团队不应该由一个个职能部门组成,同一个团队中应该有客户、业务分析师、程序员、测试人员、经理等等。同时这些所有角色应该尽量处于同一个房间中。这个实践的目的是无限缩短这些角色之间的物理距离,打破只能壁垒。
这样可以极大地提高团队效率,消息不用层层传递,有相应的问题直接能够找到对应的角色进行解决,从问题发生、提出到开始讨论只有短短几秒钟。
而且团队坐在一起总会触发神秘力量。客户可以无意从程序员或测试人员屏幕上立马发现不对的东西;一场激烈讨论中,可能坐在旁边的另外一个角色本来就有正确答案;两个程序员对需求的描述被业务分析师发现与自己的想法不符......
需要注意的是,虽然这个实践一直在描述团队的行为,但这的确是一个业务实践,因为这个实践受益最大的是业务,当团队在同一地点时,业务运行会更加流畅。
远程办公的今天
在今天,日新月异的技术已经让远程办公成为了可能,特别是视频会议的兴起一定程度上弥补了物理上无法坐在一起办公的遗憾,甚至编译器已经让远程结对做到毫无延迟。
敏捷团队可以分散在各处吗?在之前这个答案是未知的,但经历了2020年的疫情,这个答案大概是肯定的。至少我们团队和我们各个办公室的团队做的都挺好,但这的确需要付出更多的精力和拿出更多的纪律和规则。
可以肯定的是,这是可行的,但是如果大家都在同一个房间,我们可以做的更好。