我于2001年以工程总监的身份加入Google。当时,Google大概有200名开发人员,但只有区区3位测试人员!那个时候,开发人员已经开始做自己代码的测试了,但由于测试驱动开发的模式才刚刚开始,而且像JUnit这样的测试框架也没有大规模使用。当时的测试主要是在做一些随机测试(ad-hoc testing),其好坏取决于编写代码的开发者的责任心。但即使那样也是可以接受的,因为,当时正处在创业阶段,必须快速前进并勇于冒险,否则就无法和那个时代已经非常强大的对手竞争。
然而,当Google逐渐成长变大,Google的一些产品对于最终用户和客户来说开始变得至关重要(例如,竞价广告产品,我曾经负责的产品,很快变成许多网站的主要收入来源),我们清晰地认识到必须加大对测试的关注和投入。但只有3个测试工程师,别无选择,只能让开发来做更多的测试。与其他的几个Googler(译注:Google员工,本书中一般指Google工程师)一起,我们介绍、培训、推行单元测试,我们鼓励开发人员把测试作为优先级较高的事去做,并建议使用一些工具,如JUnit,把测试做成自动化的。但是进展缓慢.
开发人员发现,为了测试充分,他们不得不针对每一行功能代码,写两到三行的单元测试代码,而且这些测试代码和功能代码一样都需要维护,且有着相同的出错概率。而且大家也意识到,仅做单元测试是不够的,仍然需要集成测试、系统测试、用户界面等方面的测试。当真正开始要去做测试的时候,会发现测试工作量变得非常大(且需要很多知识的学习),并要求在很短的时间内完成测试,要以"迅雷不及掩耳"之势完成。
我们为什么要在很短的时间内迅速地完成测试呢? 我一直这么认为,对于一个坏点子或考虑欠周的产品,即便再多的测试,也无法把它变成一个成功的产品。但如果测试方法不当,却会扼杀一个本来有机会成功的产品或公司,至少会拖慢这个产品的速度,让竞争对手有机可乘。
- Patrick Copeland 谷歌测试和部署技术的架构师
我在Google的旅程始于2005年3月。Alberto在前面的序中也介绍了一些当时Google的状况:虽然公司规模还比较小,但已开始感受到成长带来的烦恼。当时适逢快速的技术变革之际,Web世界正在迎接动态内容的到来,而云计算也正在逐渐成为一种新的选择,取代当时还占统治地位的客户机-服务器架构
我加入Google的时候,工程团队还不足1000人。测试团队大概有50名全职人员和一些临时工,具体数量我一直没搞清楚。测试团队当时的称谓是"测试服务",工作重点在UI的验证上,随时响应不同项目的测试需求。可以想象,这并不是Google最闪耀的团队。
但这在当时已经足够了。Google当时的主要业务是搜索和广告,规模要比今天小得多,一次彻底的探索式测试足以发现绝大多数的质量问题。然而,世界在变,Web点击量开始史无前例地爆发性增长,文档化的Web正在让位于应用化的Web。你可以感觉到势不可挡的成长和变化,在这种情况下,规模化和快速进入市场的能力变得至关重要和生死攸关。
在Google内部,规模和问题的复杂性给测试服务团队带来了巨大的压力。在之前小型的、类同的项目里的一些可行做法,现在却让优秀的测试人员感到筋疲力尽,疲于奔命在多个急需救火的项目之间。更加火上浇油的是,Google在项目快速发布方面的坚持。是时候采取措施了,我面临两个选择,要么沿用这种劳动密集型的流程增加更多的人手,要么改变整个游戏规则。为了适应业界和Google发生的巨变,测试服务团队需要根本性的变革。
我也很想说自己是借助于丰富的经验构思出了完美的测试组织模型,但实事求是地讲,我从过去的经历中,学到的只不过是一些过时的做法。我所工作或领导过的每个测试组织都有这样或那样的问题。有问题是常态,代码质量很糟糕,测试用例很差劲,团队也问题多多。我完全清楚那种被技术质量债压得喘不过气来的感受,在那种状态下,一切创新性的想法都会被遏制,以免不小心破坏了脆弱的产品。如果说我在以往的经历中有所收获的话,那就是经历了各种错误的测试实践。
那个时候,以我对Google的了解,有一件事情是确定无疑的,那就是Google对于计算机科学和编程能力非常重视。从根本上说,如果测试人员想加入这个俱乐部,就必须具备良好的计算机科学基础和编程能力。
变革Google测试的首要问题是重新定位身为测试人员的意义所在。我过去经常在头脑中想象理想团队的模型,想象这样的团队是如何肩负起质量重任的,每次我都会得到相同的结论:一个团队能编写出高质量软件的唯一途径是全体成员共同对质量负责,包括产品经理、开发人员、测试人员等所有人。我认为,达到此目标的最好方式是使测试人员有能力将测试变成代码库的一个实际功能,而测试功能的地位应该与真实客户看到的任何其他功能同等重要。我所需要的能够实现测试功能的技能,也正是开发人员需要具备的技能。
招聘具备开发能力的测试人员很难,找到懂测试的开发人员就更难,但是维持现状更要命,我只能往前走。我希望测试人员能为他们的产品做更多的事情,同时,我希望演变测试工作的性质和从属,要求开发团队更大地投入。这种组织结构在当时的业界尚未实现,但我坚信它非常适合Google,我相信在这家公司,时机到了。
不幸的是,这种如此深刻、根本性的变革在公司里极度缺乏认同,极少有人能分享我的激情。当我开始推销这种关于软件测试角色的地位平等而作用不同的愿景时,我发现竟然难以找到一个人一起共享午餐!开发工程师们好像被他们将要在测试上发挥更大的作用这个想法吓着了,他们指出"这是测试人员的职责"。而测试人员也不买账,因为很多人已经习惯了当前的角色,维持现状的惯性导致任何变革都变得非常困难。
我毫不松懈地继续努力着,主要是出于对Google的研发过程深陷技术和质量债的困境的恐惧,一旦如此,长达5年的开发周期又会成为现实,而我本来已经很高兴地把它们留在客户机-服务器的世界里了。Google是一家由天才组成的公司,以创新为灵魂,这种企业文化与冗长的开发周期是不相容的。这是一场值得打的战斗,我说服自己,一旦这些天才理解了这种旨在打造一个生产线式的、可重复的"技术工厂"的开发和测试实践 ,他们就会改变看法。他们就会理解我们不再是一个初创公司,快速成长的用户群、不断累积的bug和糟糕结构的代码形成的技术债将会导致开发过程的崩溃。
我逐个接触各产品团队,寻找优秀的案例,试图为我的立论找到比较容易的切入点。在开发人员面前,我描绘了一个持续构建、快速部署的蓝图,一个行动敏捷、省下更多时间用于创新的开发过程;在测试人员面前,我激发他们对于成为同等技能、同等贡献和同等薪酬的完全的工程合作伙伴的渴望。
开发人员的态度是,如果我们招聘到有能力做功能开发的人,那么,我们应当让他们做功能开发。其中一些人对我的想法非常反感,甚至发信给我的主管,非常直率地建议如何来处理我的疯狂之举,这些信塞满了我的主管的邮箱。幸运的是,我的主管并没有采纳那些建议。
令我吃惊的是,测试人员的反应竟然与开发人员类似。他们沉湎于老的做事方式,抱怨自己在开发面前的地位,但又不想去改变。
我的主管对这些抱怨只有一句话:"这里是Google,如果你有想法,尽管去做就是。"
于是我开始付诸行动。我召集了一批志同道合的骨干分子,组成了一个面试团队,开始招聘。事情进行得比较艰难,我们寻找的人要兼具开发人员的技能和测试人员的思维,他们必须会编程,能实现工具、平台和测试自动化。我们必须对招聘和面试的标准与流程做出一些调整,并向已经习惯了既有模式的招聘委员会做出合理解释。
最初的几个季度进行得异常艰难。好的候选人经常在面试过程中失利,也许是因为他们没能很快地解决一些奇怪的编程问题,或是在某些人认为很重要的方面表现得不够好(然而这些方面其实与测试技能毫不相干)。我预料到了招聘过程的困难,每周都要抽出大量时间写辩词。这些辩词最终会到达Google联合创始人Larry Page手里(他一直是招聘的最终批准者)。他批准了足够多的候选人,我的团队开始稳步增长。直到现在,我猜每次Larry听到我的名字时想到的一定是:"招聘测试的!"
当然,到这个时候,我已经做了大量的宣传和鼓动工作,来说服大家这是唯一的选择。整个公司都在看着我们,一旦失败,后果将是灾难性的。对于一个混合了很多不断变化的外包人员和临时人员的小测试团队而言,期望显得如此之高。然而,即使是在我们艰难的招聘进行中同时减少了临时人员的数量时,我已经注意到了变化在发生。测试资源越稀缺,给开发人员留下的测试工作就越多。很多团队都勇敢地接受了挑战。我感觉,如果技术保持不变的话,这个时候的状态已经在接近我们的目标了。
然而,技术不是静止不动的,开发和测试实践处于飞速的变化之中。静态Web应用的时代已经成为过去,浏览器还在努力追赶之中,围绕浏览器的自动化技术比已经迟缓的浏览器还要落后一年。开发人员正面临着巨大的技术变革,在这个时候,把测试交给开发人员,这看上去是徒劳的。我们甚至还不太会手工测试这些应用,更不用提自动化测试了。
开发团队身上的压力也同样巨大。当时Google 开始收购拥有富含动态Web应用的公司。YouTube、Google Docs等后继产品的融入,延展了我们内部的基础设施。开发团队在编写功能代码的过程中,要面临很多问题,与我们测试人员在测试过程中要面临的问题一样,令人生畏!测试人员面对的测试问题无法孤立地解决。把测试和开发割裂开来,看成两个单独的环节,甚至是两类截然不同的问题,这种做法是错误的,沿着这条路走下去意味着什么问题也解决不了。解决测试团队的问题,只是我们前进路上的其中一步而已。
进展在继续。雇佣优秀的人是一件很有意思的事情,他们会推动进展的发生!到了2007年,测试团队有了更好的定位。我们能够很好地处理发布周期的最后环节。开发团队已经视我们为顺利上线的可靠合作伙伴。不过我们仍然是在发布过程的后期才介入的支持团队,局限于传统QA模型。尽管有了优秀的执行能力,我们还没达到我设想的目标。我解决了招聘方面的问题,测试也向着正确的方向发展,但是我们还是在整个流程中介入太晚。
我们在一个被称作"测试认证"(本书后面的章节会详细介绍)的事情上取得了不少进展。我们向开发团队提供咨询,帮助他们改善代码质量并尽早进行单元测试。我们开发工具并指导团队进行持续集成,使产品一直保持可测试的状态。我们进行了无数的改进和调整,从而消除了之前的很多质疑,本书详细介绍了其中的很多方法。但是,在那个时候,还是感觉缺乏整体感,开发依旧是开发,测试依旧是测试。虽然很多文化变革的因素已经存在,但是,我们还需要一个催化剂把它们聚合成一体。
自从根据我的想法开始招聘担当测试角色的开发人员以来,测试组织在不断壮大。基于对这个团队的思考,我意识到测试仅仅是我们所负责的工作的一部分。我们的工具团队开发了从源代码库到编译框架,再到缺陷数据库的各种工具。我们是测试工程师、发布工程师、工具开发工程师和咨询师。触动我的是,我们所做的非测试的工作对生产力的提升产生了巨大的影响力。我们的名称是测试服务,但是我们的职责已经远大于此。
因此,我决定正式把团队名称改为工程生产力(Engineering Productivity)团队。伴随着称谓的改变,随之而来的是文化的革新。人们开始更多地谈论生产力而不是测试和质量。生产力是我们的工作,测试和质量是开发过程里每个人都要承担的工作。这意味着开发人员负责测试,开发人员负责质量。生产力团队负责帮助开发团队搞定这两项任务。
开始的时候,这个观点还只是一种梦想和志向,我们提出的"给Google加速"的口号听起来也很空洞,但是,随着时间的推移和我们的努力,我们实现了这些诺言。我们的工具让开发的动作更快,我们帮助开发人员扫清了一个又一个障碍,消除了一个又一个瓶颈。我们的工具还使开发人员能够编写测试用例,并在每次构建时看到这些测试的结果反馈。测试用例不再只是隔离地运行在某些测试人员的机器上。测试结果会在仪表盘上显示,并把成功的版本积累下来,作为应用发布健康性的公开数据。我们并不是仅仅要求开发人员对测试和质量负责,我们还提供帮助让他们可以轻松地达到这些要求。生产力和测试的区别最终变成了现实--Google的创新能够更为顺畅,技术债也不会累积了。
最终结果如何呢?我可不愿这么早就交了底,因为这本书就是要详细讲述这个问题的。作者们花费了巨大精力,根据自身和其他Googler的经历,把我们的秘诀浓缩成了一套核心实践。但其实,我们的成功有很多方面,从将构建次数以数量级式地降低,到"跑完即忘"式的测试自动化,再到开源一些非常新颖的测试工具。在我写这篇序的时候,生产力团队已经拥有1200名工程师,这个数量比我在2005年加入Google时整个工程部门的工程师的数量还要多。生产力品牌的影响力已经相当大,我们加速Google的使命已经作为工程文化的一部分,被广泛接受。从我困惑、迷茫地坐在TGIF会议上的第一天到现在,这个团队已经走过了漫长的征途。这期间唯一没变的是我那顶三色螺旋桨帽,我把它放在我的桌上,作为我们一路走来的见证。
Patrick Copeland是Google工程生产力部门的高级总监,处于Google整个测试链的最顶端。公司里所有的测试人员都最终汇报给Patrick(而他恰好跨级汇报给Larry Page,Google的联合创始人和CEO)。Patrick加入Google之前是微软的测试总监,并在那里工作了近10年。他经常公开演讲,在Google内部被公认为Google软件快速开发、测试和部署技术的架构师。
Google软件测试介绍
- 质量不等于测试
有时,测试和开发互相交织在一起,达到了无法区分彼此的程度,而在另外一些时候,测试和开发又是完全分离的,甚至开发人员都不知道测试在做些什么
质量不等于测试。当你把开发过程和测试放到一起,就像在搅拌机里混合搅拌那样,直到不能区分彼此的时候,你就得到了质量。
- 角色
软件开发工程师(译注:software engineer,后文简称SWE)是一个传统上的开发角色,他们的工作是实现最终用户使用的功能代码。他们创建设计文档、选择最优的数据结构和整体架构,并且花费大量时间在代码实现与代码审查上。SWE需要编写与测试代码,包括测试驱动的设计、单元测试、参与构建各种规模的测试等,这些测试会在本章的后面做详细解释。SWE会对他们编写、修复以及修改的代码承担质量责任。如果一个开发者不得不修改一个函数,或者这次修改导致已有测试用例运行失败,或者需要增加一个新的测试用例,他就必须去实现这个测试用例的代码。开发工程师几乎将所有的时间都花费在了代码编写上。
软件测试开发工程师(译注:software engineer in test,后文简称SET)也是一个开发角色,只是工作重心在可测试性和通用测试基础框架上。他们参与设计评审,非常近距离地观察代码质量与风险。为了增加可测试性,他们甚至会对代码进行重构,并编写单元测试框架和自动化测试框架。SET是SWE在代码库上的合作伙伴,相比较SWE是在增加功能性代码或是提高性能的代码,SET更加关注于质量提升和测试覆盖率的增加。SET同样会花费近百分之百的时间在编写代码上,他们这样做的目的是为质量服务,而SWE则更关注在客户使用功能开发的实现上。
测试工程师(译注:test engineer,后文简称TE)是一个和SET关系密切的角色,有自己不同的关注点--把用户放在第一位来思考,代表用户的利益。一些Google的TE会花费大量时间在模拟用户的使用场景和自动化脚本或代码上。同时,他们会把开发工程师和SET编写的测试分门别类地组织起来,分析、解释、测试运行结果,驱动测试执行,特别是在项目的最后阶段,推进产品发布。TE是真正的产品专家、质量顾问和风险分析师。某些TE需要编写大量的代码,而另外一些TE则只用编写少量的代码。
- 组织结构
测试是独立存在的部门,是与专注领域部门平行的部门(横跨各个产品专注领域),我们称之为工程生产力团队。测试人员基本上以租借的方式进入到产品团队,去做提高质量相关的事情,寻找一些测试不足的地方,或者公开一些不可接受的缺陷率数据。由于测试人员并不是直接向产品团队进行汇报,因此我们并不是简单地被告之某个项目急需发布就可以通过测试。我们有自己选择决定的优先级,在可靠性、安全性等问题上都不会妥协,除非碰到更重要的事情。如果开发团队想要我们在测试上放他们一马,他们必须事先和我们协商,但一般情况下都会被拒绝。
注意 工程生产力团队会根据不同产品团队的优先级、复杂度和其他产品的实际比较后,再来分配测试人员。显然,有时候我们可能搞错,实际上也确实出过错,但总体上来说,这样会保持实际的需求与不明确的需求之间的某种平衡。
这种测试人员在不同项目之间的借调模式,可以让SET和TE时刻保持新鲜感并且总是很忙碌,另外还能保证一个好的测试想法可以快速在公司内部蔓延。一个在Geo产品上运用很好的测试技术或工具,很有可能在Chrome产品中也得到使用。推广测试技术方面创新的最佳方式,莫过于把这个创新的发明者直接借调过来。
在拥有如此少量测试人员的情况下,Google还可以取得不错的成果,核心原因在于Google从来不会在一次发布的产品中包含大量功能。
- 版本管理
金丝雀版本、开发版本、测试版本、beta或发布版本、
- 测试类型
小型测试 一般来说(但也并非所有)都是自动化实现的,用于验证一个单独函数或独立功能模块的代码是否按照预期工作,着重于典型功能性问题、数据损坏、错误条件和大小差一错误等方面的验证。小型测试的运行时间一般比较短,通常是在几秒或更短的时间内就可以运行完毕。通常,小型测试是由SWE来实现,也会有少量的SET参与,TE几乎不参与小型测试。小型测试一般需要使用mock和fake(译注:mock fake环境是实际依赖系统的替代者,会提供相应的功能,但这些系统可能不存在,或者缺陷太多不可靠,或者是一些很难模拟的错误条件)才能运行。TE几乎不编写小型测试代码,但会参与运行这些测试,来诊断一些特定错误。小型测试主要尝试解决的问题是"这些代码是否按照预期的方式运行"。
中型测试 通常也都是自动化实现的。该测试一般会涉及两个或两个以上,甚至更多模块之间的交互。测试重点在于验证这些"功能近邻区"之间的交互,以及彼此调用时的功能是否正确(我们称功能交互区域为"功能近邻区")。在产品早期开发过程中,在独立模块功能被开发完毕之后,SET会驱动这些测试的实现及运行,SWE会深度参与,一起编码、调试和维护这些测试。如果一个中型测试运行失败,SWE会自觉地去查看分析原因。在开发过程的后期,TE会通过手动的方式(如果比较难去实现自动化或实现的代价较大时),或者自动化地执行这些用例。中型测试尝试去解决的问题是,一系列临近的模块互相交互的时候,是否如我们预期的那样工作。
大型测试 涵盖三个或以上(通常更多)的功能模块,使用真实用户使用场景和实际用户数据,一般可能需要消耗数个小时或更长的时间才能运行完成。大型测试关注的是所有模块的集成,但更倾向于结果驱动,验证软件是否满足最终用户的需求。所有的三种工程师角色都会参与到大型测试之中,或是通过自动化测试,或是探索式测试。大型测试尝试去解决的问题是,这个产品操作运行方式是否和用户的期望相同,并产生预期的结果。这种端到端的使用场景以及在整体产品或服务之上的操作行为,即是大型测试关注的重点。
关于自动化测试和手动测试的比例,对于所有的三种类型测试,当然更倾向于前者。如果能够自动化,并不需要人脑的智睿与直觉来判断,那就应该以自动化的方式实现。但在一些情况下需要人类智慧的判断,如用户界面是否漂亮、保留的数据是否包含隐私等方面,这些还是需要手动测试来完成。
软件测试开发工程师
对于功能代码而言,思维模式是创建,重点在考虑用户、使用场景和数据流程上;而对于测试代码来说,主要思路是去破坏,怎样写测试代码用以扰乱分离用户及其数据。由于我们假设的前提是在一个童话般的理想开发过程里,所以我们或许可以分别雇佣不同的开发工程师:一个写功能代码,而另一个思考如何破坏这些功能(译注:两种开发工程师,分别是功能开发人员和测试开发人员)。
功能开发人员在编写功能代码的时候,测试开发人员编写测试代码,但我们还需要第三种角色,一个关心真正用户的角色。显然在我们理想化的乌托邦测试世界里,这个工作应该由第三种工程师来完成,既不是功能开发人员,也不是测试开发人员。我们把这个新角色称为用户开发人员(译注:user developer)。他们需要解决的主要问题是面向用户的任务,包括用例(use case)、用户故事、用户场景、探索式测试等。用户开发人员关心这些功能模块如何集成在一起成为一个完整的整体,他们主要考虑系统级别的问题,通常情况下都会从用户角度出发,验证独立模块集成在一起之后是否对最终用户产生价值。
SET的工作
- 开发和测试流程
工程师团队的交付物就是即将发布的代码。代码的组织形式、开发过程、维护是日常工作重点。
公开的代码库、和谐的工程工具、公司范围内的资源共享,成就了丰富的Google内部共享代码库与公共服务。这些共享的代码运行依赖于Google的基础设施产品,它们在加速项目完成与减少项目失败上发挥了很大作用。
工程师们对这些共享的基础代码做了特殊处理,形成了一套不成文但却非常重要的实践规则,工程师在维护修改这些代码的时候都要遵守这些规则。
* 所有的工程师必须复用已经存在的公共库,除非在项目特定需求方面有很好的理由。
* 对于公共的共享代码,首先要考虑的是能否可以容易地被找到,并具有良好的可读性。代码必须存储在代码库的共享区域,以便查找。由于共享代码会被不同的工程师使用,这些代码应该容易理解。所有的代码都要考虑到未来会被其他人阅读或修改。
* 公共代码必须尽可能地被复用且相对独立。如果一个工程师提供的服务被许多团队使用,这将为他带来很高的信誉。与功能的复杂性或设计的巧妙性相比,可复用性带来的价值更大。
* 所有依赖必须明确指出,不可被忽视。如果一个项目依赖一些公用共享代码,在项目工程师不知情的前提下,这些共享代码是不允许被修改的。
* 如果一个工程师对共享代码库在某些地方有更好的解决方案,他需要去重构已有的代码,并协助依赖在这个公用代码库之上的应用项目迁移到新的代码库上。这种乐善好施的社区工作是值得鼓励的(译注:这是Google经常提及的“同僚奖金(peer bonus)”。任何工程师如果受到其他工程师正面的影响,就可以送出“同僚奖金”作为感谢。除此之外,经理还有权使用其他奖励手段。这样做的目的就是让这种正向团队合作形成一种良性循环,并持续下去。当然,另外还有同事之间私下里的感谢)。
* Google非常重视代码审核,特别是公共通用模块的代码必须经过审核。开发人员必须通过相关语言的可读性审核。在开发人员拥有按照代码风格编写出干净代码的记录之后,委员会会授予这名开发人员一个“良好可读性”的证书。Google的四大主要开发语言:C++、Java、Python和JavaScript都有可读性方面的代码风格指南。
* 在共享代码库里的代码,对测试有更高的要求(在后面部分会做讨论)。
最小化对平台的依赖。所有工程师都有一台桌面工作机器,且操作系统都尽可能地与Google生产环境的操作系统保持一致。为了减少对平台的依赖,Google对Linux发行版本的管理也十分谨慎,这样开发人员在自己工作机器上测试的结果,与生产系统里的测试结果会保持一致。从桌面到数据中心,CPU和OS的变化尽可能小(注:唯一不在Google通用测试平台里的本地测试实验室,是Android和Chrome OS。这些类目不同的硬件必须在手边进行测试)如果一个bug在测试机器上出现,那么在开发机器上和生产环境的机器上也都应该能够复现。
所有对平台有依赖的代码,都会强制要求使用公共的底层库。维护Linux发行版本的团队同时也在维护这个底层平台相关的公共库。还有一点,对于Google使用的每个编程语言,都要求使用统一的编译器,这个编译器被很好地维护着,针对不同的Linux发行版本都会有持续的测试。这样做本身其实并没有什么神奇之处,但限制运行环境可以节省大量下游的测试工作,也可以避免许多与环境相关且难以调试的问题,能把开发人员的重心转移到新功能开发上。保持简单,也就相对会安全。
Google在平台方面有特定的目标,就是保持简单且统一。开发工作机和生产环境的机器都保持统一的Linux发行版本;一套集中控制的通用核心库;一套统一的通用代码、构建和测试基础设施;每个核心语言只有一个编译器;与语言无关的通用打包规范;文化上对这些共享资源的维护表示尊重且有激励。
使用统一的运行平台和相同的代码库,持续不断地在构建系统中打包(译注:打包是一个过程,包括将源代码编译成二进制文件,然后再把二进制文件统一封装在一个linux rpm包里面),这可以简化共享代码的维护工作。构建系统要求使用统一的打包规范,这个打包规范与项目特定的编程语言无关,与团队是否使用C++、Python或Java也都无关。大家使用同样的“构建文件”来打包生成二进制文件。
一个版本在构建的时候需要指定构建目标,这个构建目标(可以是公共库、二进制文件或测试套件)由许多源文件编译链接产生。下面是整体流程。
* (1)针对某个服务,在一个或多个源代码文件中编写一类或一系列功能函数,并保证所有代码可以编译通过。
* (2)把这个新服务的构建目标设定为公共库。
* (3)通过调用这个库的方式编写一套单元测试用例,把外部重要依赖通过mock模拟实现。对于需要关注的代码路径,使用最常见的输入参数来验证。
* (4)为单元测试创建一个测试构建目标。
* (5)构建并运行测试目标,做适当的修改调整,直到所有的测试都运行成功。
* (6)按要求运行静态代码分析工具,确保遵守统一的代码风格,且通过一系列常见问题的静态扫描检测。
* (7)提交代码申请代码审核(后面对代码审核会做更多详细说明),根据反馈再做适当的修改,然后运行所有的单元测试并保证顺利通过。
产出将是两个配套的构建目标:库构建目标和测试构建目标。库构建目标是需要新发布的公共库、测试构建目标用以验证新发布的公共库是否满足需求。注意:在Google许多开发人员使用“测试驱动开发”的模式,这意味着步骤(3)会在步骤(1)和步骤(2)之前进行。
对于规模更大的服务,通过链接编译持续新增的代码,构建目标也会逐渐变大,直到整个服务全部构建完成。在这个时候,会产生二进制构建目标,其由包含主入口main函数文件和服务库链接在一起构成。现在,你完成了一个Google产品,它由三部分组成:一个经过良好测试的独立库、一个在可读性与可复用性方面都不错的公共服务库(这个服务库中还包含另外一套支持库,可以用来创建其他的服务)、一套覆盖所有重要构建目标的单元测试套件。
一个典型的Google产品由许多服务组成,所有产品团队都希望一个SWE负责对应一个服务。这意味着每个服务都可以并行地构建、打包和测试,一旦所有的服务都完成了,他们会在一个最终的构建目标里一起集成。为了保证单独的服务可以并行地开发,服务之间的接口需要在项目的早期就确定下来。这样,开发者会依赖在协商好的接口上,而不是依赖在需要开发的特定库上。为了不耽搁服务级别之间的早期测试,这些接口一般都不会真正实现,而只是做一个虚假的实现。
SET会参与到许多测试目标的构建之中,并指出哪些地方需要小型测试。在多个构建目标集成在一起,形成规模更大应用程序的构建目标时,SET需要加速他们的工作,开始做一些更大规模的集成测试。在一个单独的库构建目标中,需要运行几乎所有的小型测试(由SWE编写,所有支持这个项目的SET都会给予帮助)。当构建目标日益增大时,SET也会参与到中大型测试的编写之中去。
- SET是什么?
SET是开发,主要做可测试性方面的工作。
SET首先是工程师角色,他使得测试存活于先前讨论的所有Google开发过程之中。SET(software engineer in test)是软件测试开发工程师。最重要的一点,SET是软件工程师,正如我们招聘宣传海报和内部晋升体系中所说的那样,是一个100%的编码角色。这种测试方式的有趣之处在于它使测试人员能尽早介入到开发流程中去,但不是通过“质量模型”和“测试计划”的方式,而是通过参与设计和代码开发的方式。这会使得功能的开发工程师和测试的开发工程师处于相同的地位,SET积极参与各种测试,使测试富有效率,包括手动测试和探索式测试,而这些测试后期会由其他工程师负责。
- SET的介入时机
在项目早期,Google一般不会让测试介入进来。实际上,即使SET在早期参与进来,也不是从事测试工作,而是去做开发。绝非有意忽视测试,当然也不是说早期产品的质量就不重要。这是受Google非正式创新驱动产品的流程所约束。Google很少在项目创建初期就投入一大帮人来做计划(包括质量与测试计划),然后再让一大群开发参与进来。Google项目的诞生从来没有如此正式过。
- 团队结构
Google产品团队最初是由一个技术负责人(tech lead)和一个或更多的项目发起人组成。在Google,技术负责人这个非正式的岗位一般由工程师担任,负责设定技术方向、开展合作、充当与其他团队沟通的项目接口人。他知道关于项目的任何问题,或者能够指出谁知道这些问题的细节。技术负责人通常是一名SWE,或者由一名具备SWE能力的工程师来担任。
- 设计文档
所有Google项目都有设计文档。这是一个动态的文档,随着项目的演化也在不断地保持更新。最早期的项目设计文档,主要包括项目的目标、背景、团队成员、系统设计。在初期阶段,团队成员一起协同完成设计文档的不同部分。对于一些规模足够大的项目来说,需要针对主要子系统也创建相应的设计文档,并在项目设计文档中增加子系统设计文档的链接。在初期版本完成后,里面会囊括所有将来需要完成的工作清单,这也可以作为项目前进的路标。从这一点上讲,设计文档必须要经过相关技术负责人的审核。在项目设计文档得到足够的评审与反馈之后,初期版本的设计文档就接近尾声了,接下来项目就正式进入实施阶段。
作为SET,比较幸运的是在初期阶段就加入了项目,会有一些重要且有影响力的工作急需完成。如果能够合理地谋划策略,我们在加速项目进度的同时,也可以做到简化项目相关人员的工作。实际上,作为工程师,SET在团队中有一个巨大的优势,就是拥有产品方面最广阔的视野。一个好的SET会把非常专业的广阔视野转化成影响力,在开发人员所编写的代码上产生深远的影响力。通常来说,代码复用和模块交互方面的设计会由SET来做,而不是SWE。后面会着重介绍SET在项目的初期阶段是如何发挥作用的。
SET需要熟悉了解所负责的系统设计(阅读所有的设计文档是一个途径),SET和SWE都期望如此。
SET早期提出的建议会反馈在文档和代码里,这样也增加了SET的整体影响力。
作为第一个审阅所有设计文档的人(也因此了解所有迭代过程),SET对项目的整体了解程度超过了技术负责人。
对于SET来说,这也是一个非常好的机会,可以在项目初期就与相应开发工程师一起建立良好的工作关系。
审阅设计文档的时候应该有一定的目的性,而不是像读报纸那样随便看两眼就算了。优秀的SET在审阅过程中始终保持强烈的目的性。下面是一些我们推荐的一些要点。
#!python
完整性:找出文档中残缺不全或一些需要特殊背景知识的地方。通常情况下团队里没人会了解这些知识,特别是对新人而言。鼓励文档作者在这方面添加更多细节,或增加一些外部文档链接,用以补充这部分背景知识。
正确性:看一下是否有语法、拼写、标点符号等方面的错误,这一般是马虎大意造成的,并不意味着他们以后编写的代码也是这样。但也不能为这种错误而破坏规矩。
一致性:确保配图和文字描述一致。确保文档中没有出现与其他文档中截然相反的观点和主张。
设计:文档中的一些设计要经过深思熟虑。考虑到可用的资源,目标是否可以顺利达成?要使用何种基础的技术框架(读一读框架文档并了解他们的不足)?期望的设计在框架方面使用方法上是否正确?设计是否太过复杂?有可能简化吗?还是太简单了?这个设计还需要增加什么内容?
接口与协议:文档中是否对所使用的协议有清晰的定义?是否完整地描述了产品对外的接口与协议?这些接口协议的实现是否与他们期望的那样一致?对于其他的Google产品是否满足统一的标准?是否鼓励开发人员自定义Protocol buffer数据格式(后面会讨论Protocol buffer)?
测试:系统或文档中描述的整套系统的可测试性怎样?是否需要新增测试钩子(译注:testing hook,这里指为了测试而增加一些接口,用以显示系统内部状态信息)?如果需要,确保他们也被添加到文档之中。系统的设计是否考虑到易测试性,而为之也做了一些调整?是否可以使用已有的测试框架?预估一下在测试方面我们都需要做哪些工作,并把这部分内容也增加到设计文档中去。
google的代码评审工具 https://github.com/rietveld-codereview/rietveld
- 接口协议
Google protocol buffer语言(注:Google protocol buffers 是开源的,参见http://code.google.com/apis/protocolbuffers)
为了能够尽早可以运行集成测试,针对依赖服务,SET提供了mock与fake。
- 自动化计划
我们首先把容易出错的接口做隔离,并针对它们创建mock和fake(在之前的章节中做过介绍),这样我们可以控制这些接口之间的交互,确保良好的测试覆盖率。
接下来构建一个轻量级的自动化框架,控制mock系统的创建和执行。这样的话,写代码的SWE可以使用这些mock接口来做一个私有构建。在他们把修改的代码提交到代码服务器之前运行相应的自动化测试,可以确保只有经过良好测试的代码才能被提交到代码库中。这是自动化测试擅长的地方,保证生态系统远离糟糕代码,并确保代码库永远处于一个时刻干净的状态。
SET除了在这个计划中涵盖自动化(mock、fake和框架)之外,还要包括如何公开产品质量方面的信息给所有关心的人。在Google,SET使用报表和仪表盘(译注:dashboard)来展示收集到的测试结果以及测试进度。通过将整个过程简化和信息公开透明化,获取高质量代码的概率会大大增加。
- 可测试性
为了使SET也成为源码的拥有者之一,Google把代码审查作为开发流程的中心。相比较编写代码而言,代码审查更值得炫耀。
在CL提交审查之前,会经过一系列的自动化检查。这种自动化静态检查所使用的规则包含一些简单的确认,例如是否遵循Google的代码风格指南、提交CL相关的测试用例是否执行通过(原则上所有的测试必须全部通过)等。CL里面一般总是包含针对这个CL的测试代码,测试代码总是和功能代码在一起。在检查完成之后,Mondrian会给相应的CL审阅者发送一封包含这个CL链接的通知邮件。随后审阅者会进行代码审查,并把修改建议发回给SWE去处理。这个过程会反复进行,直到提交者和审阅者都满意为止。
提交队列(译注:submit queue)的主要功能是保持“绿色”的构建,这意味着所有测试必须全部通过。这是构建系统和版本控制系统之间的最后一道防线。通过在干净环境中编译代码并运行测试,提交队列系统可以捕获在开发机器上无法发现的环境错误,但这会导致构建失败,甚至是导致版本控制系统中的代码处于不可编译的状态。
规模较大的团队可以利用提交队列在同一个代码分支上进行开发。如果没有提交队列,通常在代码集成或每轮测试时都会把代码冻结,使用提交队列就可以避免这个问题。在这种模式下,提交队列可以使得规模较大团队就像小团队一样,高效且独立。由于这样增加了开发提交代码的频率,势必给SET的工作带来了较大难度,这可能是唯一的弊端。
- SET的工作流程:一个实例:
略,可以参考具体语言的单元测试。
- 测试执行
做代码编译、测试执行、结果分析、数据存储、报表展示的通用的测试框架
- 测试规模
小型测试是为了验证一个代码单元的功能,一般与运行环境隔离,例如针对一个独立的类或一组相关函数的测试。小型测试的运行不需要外部依赖。在Google之外,小型测试通常就是单元测试。
中型测试是验证两个或多个模块应用之间的交互。和小型测试相比,中型测试有着更大的范畴且运行所需要的时间也更久。小型测试会尝试走遍单独函数的所有路径,而中型测试的主要目标是验证指定模块之间的交互。在Google之外,中型测试经常被称为“集成测试”。
在Google之外通常被称为“系统测试”或“端到端测试”。大型测试在一个较高层次上运行,验证系统作为一个整体是如何工作的。这涉及应用系统的一个或所有子系统,从前端界面到后端数据储存。该测试也可能会依赖外部资源,如数据库、文件系统、网络服务等。
小型测试是为了验证一个代码单元的功能。中型测试验证两个或多个模块应用之间的交互。大型测试是为了验证整个系统作为一个整体是如何工作的。
- 测试平台对测试规模的调度
使用Google测试执行平台运行的一些通用任务如下。
#!python
开发人员编译和运行小型测试,希望立刻就能知道运行结果。
开发人员希望运行一个项目的所有小型测试,并能够快速知道运行结果。
开发人员只有在变更代码出现时,才希望去编译运行相关的项目测试,并即刻得到运行结果。
工程师希望能够知道一个项目的测试覆盖率并查看结果。
对项目的每次代码变更(CL),都能够运行这个项目的小型测试,并将运行结果发送给团队成员以辅助进行代码审查。
在代码变更(CL)提交到版本控制系统之后,自动运行项目的所有测试。
团队希望每周都能得到代码覆盖率,并实时跟踪覆盖率的变化。
上面提及的所有任务,有可能同时并发提交到Google测试执行系统。一些测试可能极度消耗资源,使得公用测试机器处于不可用状态达数小时。另外一些测试可能只需几毫秒,而且可以和其他几百个任务同时在一台机器上并发运行。当每一个测试都被标记为小型、中型、大型的时候,调度运行这些测试任务就会变得相对简单一些,因为调度器已经知道每个任务需要运行的时间,这样可以优化任务队列,达到合理利用的目的。
Google测试执行系统利用了测试规模的定义,把运行较快的任务从较慢的任务中挑选出来。测试规模在测试运行时间上规定了一个最大值,如表2.1所示;同时测试规模在测试运行消耗资源上也做了要求,如表2.2所示。Google测试执行系统在发现任何测试超时,或是消耗的资源超过这个测试规模应该使用的资源时,会把这个测试任务取消掉并报告这个错误。这会迫使工程师提供合适的测试规模标签。精准的测试规模,可以使Google测试执行系统在调度时做出明智的决定。
类别 | 小型测试 | 中型测试 | 大型测试 | 超大型测试
:---- ---:|:---- ---:|:---------------------:|:---------------------:
时间目标(每个函数)| 10毫秒以内 | 1秒以内 | 尽可能快 | 尽可能快
强制时间限制 | 1分钟之后强制结束 | 5分钟之后强制结束 | 15分钟之后强制结束 | 1小时之后强制结束
资源使用
类别 | 大型测试 | 中型测试 | 小型测试
:---- ---:|:---- ---:|:---------------------:
网络服务(建立一个链接)| 是 | 仅本地 | 模拟
数据库 | 是 | 仅本地 | 模拟
访问文件系统 | 是 | 是 | 模拟
访问用户界面系统 | 是 | 不鼓励 | 模拟
系统调用 | 是 | 不鼓励 | 否
多线程 | 是 | 是 | 不鼓励
睡眠状态 | 是 | 是 | 否
系统属性 | 是 | 是 | 否
- 测试规模的优缺点
大型测试
#!python
测试最根本最重要的:在考虑外部系统的情况下应用系统是如何工作的。
由于对外部系统有依赖,因此它们是非确定性的。
很宽的测试范畴意味着如果测试运行失败,寻找精准失败根源就会比较困难。
测试数据的准备工作会非常耗时。
大型测试是较高层次的操作,如果想要走到特定的代码路径区域是不切实际的,而这一部分却是小型测试的专长。
中型测试
#!python
由于不需要使用mock技术,且不受运行时刻的限制,因此该测试是从大型测试到小型测试之间的一个过渡。
因为它们运行速度相对较快,所以可以频繁地运行它们。
它们可以在标准的开发环境中运行,因此开发人员也可以很容易运行它们。
它们依赖外部系统。
由于对外部系统有依赖,因此它们本身就有不确定性。
它们的运行速度没有小型测试快。
小型测试
#!python
为了更容易地就被测试到,代码应清晰干净、函数规模较小且重点集中。为了方便模拟,系统之间的接口需要有良好的定义。
由于它们可以很快运行完毕,因此在有代码变更发生的时候就可以立刻运行,从而可以较早地发现缺陷并提供及时的反馈。
在所有的环境下它们都可以可靠地运行。
它们有较小的测试范围,这样可以很容易地做边界场景与错误条件的测试,例如一个空指针。
它们有特定的范畴,可以很容易地隔离错误。
不要做模块之间的集成测试,这是其他类型的测试要做的事情(中型测试)。
有时候对子系统的模拟是有难度的。
使用mock或fake环境,可以不与真实的环境同步。
小型测试带来优秀的代码质量、良好的异常处理、优雅的错误报告;大中型测试会带来整体产品质量和数据验证。
检验一个项目里小型测试、中型测试和大型测试之间的比率是否健康,一个好办法是使用代码覆盖率。测试代码覆盖率可以针对小型测试、中大型测试分别单独产生报告。覆盖率报告会针对不同的项目展示一个可被接受的覆盖率结果。如果中大型测试只有20%的代码覆盖率,而小型测试有近100%的覆盖率,则说明这个项目缺乏端到端的功能验证。如果结果数字反过来了,则说明这个项目很难去做升级扩展和维护,由于小型测试较少,就需要大量的时间消耗在底层代码调试查错上。
Google有许多不同类型的项目,这些项目对测试的需求也不同,小型测试、中型测试和大型测试之间的比例随着项目团队的不同而不同。这个比例并不是固定的,总体上有一个经验法则,即70/20/10原则:70%是小型测试,20%是中型测试,10%是大型测试。如果一个项目是面向用户的,拥有较高的集成度,或者用户接口比较复杂,他们就应该有更多的中型和大型测试;如果是基础平台或者面向数据的项目,例如索引或网络爬虫,则最好有大量的小型测试,中型测试和大型测试的数量要求会少很多。
另外有一个用来监视测试覆盖率的内部工具是Harvester。Harvester是一个可视化的工具,可以记录所有项目的CL历史,并以图形化的方式展示,例如测试代码和CL中新增代码的比率、代码变更的多少、按时间的变化频率、按照开发人员的变化次数,等等。这些图形的目的是展示随着时间的变化,测试的变化趋势是怎样的。
- 测试运行要求
#!python
每个测试和其他测试之间都是独立的,使它们就能够以任意顺序来执行。
测试不做任何数据持久化方面的工作。在这些测试用例离开测试环境的时候,要保证测试环境的状态与测试用例开始执行之前的状态是一样的。
对于冲突:
#!python
两个测试都要绑定同一个端口,用以接收来自网络的数据。
两个测试需要在同一个路径下创建相同的目录。
一个测试希望创建并使用一个数据库表,而另外一个测试想删除这个数据库表。
解决方案
#!python
在测试执行系统中,让每个测试用例获取一个未被使用的端口,并让被测系统动态地绑定到这个端口上。
在测试执行之前,为每一个测试用例在临时目录下创建目录和文件,并使用独一无二的目录名。
每个测试运行在自己的数据库实例之上,使用与环境隔离的目录和端口。这些都由测试执行系统来控制。
Google全力维护其测试执行系统,甚至文档也非常详尽。这些文档存放在Google的“测试百科全书”中,这里有对其运行使用的资源所做的最终解释。“测试百科全书”有点像IEEE RFC(译注:IEEE定义的正式标准,RFC是Request for Comment的简写),明确使用“必须”或“应该”这样的字样,并在其中详细解释了角色、测试用例职责、测试执行者、集群系统、运行时刻的libc、文件系统等。
构建系统能定位到错误由哪个提交引起,同时还保存了构建依赖图。
测试认证
招聘到技术能力强的测试人员只是刚刚开始的第一步,我们依然需要开发人员参与进来一起做测试。其中我们使用的一个关键方法就是被称为“测试认证”。
测试认证级别摘要
#!python
级别1
使用测试覆盖率工具。
使用持续集成。
测试分级为小型、中型、大型。
明确标记哪些测试是非确定性的测试。
创建冒烟测试集合。
级别2
如果有测试运行结果为红色就不会做发布。
在每次代码提交之前都要求通过冒烟测试。
各种类型测试的整体增量覆盖率要大于50%。
小型测试的增量覆盖率要大于10%。
每一个功能特性至少有一个与之对应的集成测试用例。
级别3
所有重要的代码变更都要经过测试。
小型测试的增量覆盖率要大于50%。
新增的重要功能都要经过集成测试的验证。
级别4
在提交任何新代码之前都会自动运行冒烟测试。
冒烟测试必须在30分钟内运行完毕。
没有不确定性的测试。
总体测试覆盖率应该不小于40%。
小型测试的代码覆盖率应该不小于25%。
所有重要的功能都应该被集成测试验证到。
级别5
对每一个重要的缺陷修复都要增加一个测试用例与之对应。
积极使用可用的代码分析工具。
总体测试覆盖率不低于60%。
小型测试的代码覆盖率应该不小于40%。
最初这个计划在一些测试意识较高的团队中缓慢试水,这些团队成员热衷于改进他们的测试实践。经过在这几个团队的成功试验之后,一个规模更大的、公司级别的认证竞赛开始推行起来了,然后在新加入的团队中再推行这个计划就变得容易的多。
#!python
开发团队得到许多优秀测试人员的关注,这些测试人员一般都报名成为测试认证教练。在一个测试资源稀缺的文化氛围里,注册参加这个项目会吸引到比一般团队更多的测试人员的加入。
他们获得专家的指导,并学习到如何更好地编写小型测试。
他们知道哪个团队在测试上做的比较好,并向这个团队学习。
他们能够向其他的认证级别较低的团队进行炫耀。
经过公司级别的推进,绝大多数团队都在不断向前进步,并意识到这个计划的重要性。一些在这个计划中表现不错的开发总监会得到工程生产力团队的优秀反馈,而嘲笑这个计划的团队也会置自身于危险之中。换句话说,在一个测试资源相对稀缺的公司里,哪个团队会舍得与工程生产力团队疏远呢?但并非哪里都是鲜花与掌声,让运行这个计划的负责人来给我们讲述完整的故事吧。
试点团队::① 足够感兴趣;② 没有太多的冗余代码;③ 在团队中有一个测试战神(对测试足够的了解的人)。
我们宣布测试认证计划“正式启动”的时候,有15个试点团队在这个计划的不同级别上运行着。在正式宣布之前,我们在山景城、纽约和其他地点的所有办公大楼上张贴“神秘的测试认证”的大海报,每个海报上用图片印着各个试点团队名字,使用的是内部项目名称,如Rubix、Bounty、Mondrian和Red Tape。海报上唯一的文字是“未来就是现在”和“至关重要,莫被遗弃”,还有一个链接。从喜爱猜谜的Google同事那里,我们得到了大量点击访问,多数人想去一探究竟,还有一些人想去验证自己的猜测是否正确。同时我们也使用ToTT来宣传这个新计划,并把读者指引到他们能够得到信息的地方。这是一个信息闪电战。
宣传网站上有一些信息,包括为什么测试认证对于团队很重要,以及用户可以得到怎样的帮助。里面强调指出,参与团队会从一个很大的测试专家社区里得到一个测试认证教练,同时还会得到两个礼物——一个表示构建状态的发光魔法球,可以告诉团队他们的(一般是新的)持续集成是通过(绿色)还是失败(红色);另外一个是一个漂亮的星球大战土豆头工具包。这个被称为达斯土豆工具包里有三个逐渐变大的格子,每当团队达到新的测试认证级别时我们都会给予奖励。各个团队展示他们的魔法球和土豆头,为这个计划吸引来更多好奇的团队和带来更好的口碑。
测试圈子里的成员是这个项目的第一批教练和发言人。随着越来越多团队的加入,有许多热情的工程师帮助造势,自己也成为其他团队的教练。
每次我们尝试说服更多的团队加入这个计划的时候,都会与他们逐一讨论理由和原因。一些团队是由于你能使他们信服每一个级别和教练都会帮助团队在这个领域有所提高而加入的。一些团队认为他们会有所改善,并坚信这种“官方”级别评定会使他们因为当前正在做的工作得到好评。另外的一些团队,他们本身的测试成熟度已经很高了,但加入这个计划,会给其他的团队发出一种信号,表示他们已经很重视测试了。
- SET的招聘(暂略)
参考资料
- 讨论 钉钉群21745728 qq群144081101 567351477
- 本文最新版本地址
- 本文英文原版书籍
- 本文源码地址
- 本文涉及的python测试开发库 谢谢点赞!
- 本文相关海量书籍下载
网友评论