1.背景
1.1困境
团队内一位测试者对接多位开发者,开发者的需求提测速度远大于测试者的测试速度,导致开发者提测的需求堆积待测试,无法及时上线,团队测试资源匮乏的问题愈加凸显,直接影响团队的需求交付速度。
测试资源匮乏的问题在支付组中尤为严重,且支付项目业务复杂、上手周期长,要求开发者与测试者尽可能的稳定,短期内引进新人也很难解决问题。因此团队的目标是在现有资源供给下,不进行人员变动,而通过优化团队内部的质量保证工作量的分配,既保证团队稳定与研发质量,又提升需求交付速率。
1.2剖析
►1.2.1 概念
在解构团队困境之前,首先定义若干概念,建立一个简单的数学模型进行分析。
(1) 开发侧
Ø 开发资源:团队内部的开发者人数,例如10人;
Ø 单位提测速率:每个开发者平均每天提测的需求数,例如2个/人天;
Ø 提测速率:团队平均每天提测的需求数,显然有:提测速率=单位提测速率×开发资源=2×10=20个/天。
图2-提测速率公式
(2) 测试侧
Ø 测试资源:团队内部的测试者人数,例如2人;
Ø 单位测试速率:每个测试者平均每天测试完成的需求数,例如4个/人天;
Ø 测试速率:团队平均每天测试完成的需求数,显然有:测试速率=单位测试速率×测试资源=4×2=8个/天。
图3-测试速率公式
(3) 上线
Ø 需求交付速率:指团队平均每天上线交付的需求数,由于需求通过测试后基本就能上线了,显然有:需求交付速率≈测试速率=8个/天。
图4-需求交付速率
►1.2.2 提测速率与测试速率的大小关系
下面分析一下提测速率与测试速率的三种大小关系:
(1) 提测速率>测试速率
存在的问题:我们团队中目前存在这种大小关系,虽然资源都被充分利用,但资源分配不合理,导致开发者提测的需求堆积待测试,需求无法及时交付,需求交付速率与团队分配到的资源不匹配。
解决方案一:重新分配资源,减少开发资源,增加测试资源;
解决方案二:重新分配质量保证工作量,将一部分工作量由测试者转移至开发者。
(2) 提测速率<测试速率
存在的问题:资源未被充分利用,开发资源被充分利用,但测试资源出现闲置。
解决方案一:重新分配资源,增加开发资源,减少测试资源;
解决方案二:重新分配质量保证工作量,将一部分工作量由开发者转移至测试者。
(3) 提测速率≈测试速率
这是一种理想情况,资源都被充分利用,且资源分配合理,需求交付速率与团队分配到的资源匹配。
►1.2.3 提测速率与测试速率的动态平衡
很显然,我们团队的现状是:提测速率>测试速率,目标是:提测速率≈测试速率,解决方案是:提升测试速率和/或降低提测速率。
图6-现状、方案与目标
(1) 测试侧
由于测试资源固定,因此要提升测试速率,只能提升单位测试速率,即提升每个测试者平均每天测试完成的需求数,例如从4个/人天提升至6个/人天。
图7-提升测试速率
问题:如何提升单位测试速率?
这里再引入几个概念“交付能力”与“测试复杂度”:
Ø 测试复杂度:指平均每个需求需要消耗多少测试资源才能测试完成,例如2人天/个表示平均每个需求需要2个测试者花1天时间才能测试完成;
Ø 交付能力:指测试者平均每天交付工作成果的能力,交付能力与测试者的“工作能力”、“工作时长”有关,因此有如下关系:
图8-交付能力
交付能力是一个没有单位的系数,该系数越大,表示测试者的交付能力越强。
因此单位测试速率与交付能力、测试复杂度有如下关系:
图9-单位测试速率
目前测试资源都被充分利用,工作都已饱和,工作时长无法提高;工作能力的提升也不是一蹴而就的,短期内工作能力也是一个常数,因此要提升单位测试速率只能降低测试复杂度。
图10-提升单位测试速率
问题:如何降低测试复杂度?
最直观的方式就是“减少测试者测试完成每个需求的工作量”,把测试承担的一部分质量保证工作量转移出去,例如之前测试者平均需要为每个需求创建并测试10个用例,转移出去后仅需7.5个用例,这样测试复杂度就降低了25%。
那么应该把这部分质量保证工作量转移给谁,由于目前提测速率>测试速率,很显然把这部分质量保证工作量转移至开发者,下面来看一下转移之后对开发者的影响。
(2) 开发侧
对于开发者,也有交付能力这一概念,同时引入“提测复杂度”这一概念:
Ø 提测复杂度:指平均每个需求需要消耗多少开发资源才能开发完成并提测,例如2人天/个表示平均每个需求需要2个开发者花1天时间才能开发完成并提测。
开发者在开发过程中以及提测之前肯定会进行自测与基本的冒烟测试,因此开发者在完成一个需求时,除了编写代码的工作量外,还需要一定的测试工作量,因此提测复杂度由开发复杂度和测试复杂度构成,有如下关系:
图11-单位提测速率
由于把一部分质量保证工作量由测试者转移至开发者,因此开发者的测试复杂度升高了,这样就导致提测复杂度升高了,最终导致单位提测速率降低了。
图12-降低单位提测速率
随着单位提测速率的降低,开发的提测速率也就降低了。
图13-降低提测速率
在质量保证工作量转移之后,测试者的测试速率升高了,开发者的提测速率降低了,因此只要把握好转移的量,就能实现提测速率与测试速率的动态平衡。
图14-结果
1.3结论
经过分析发现,可以通过优化团队内部质量保证工作量的分配,动态地将一部分质量保证工作量由测试者转移至开发者,就能实现提测速率与测试速率的动态平衡,最终实现团队需求交付速率的最大化。
这种工作量的分配优化,其实就是团队内部资源的分配优化,即使用开发资源换取测试资源,由开发者承担更多的质量保证工作,减少对测试资源的依赖,这样就在不进行人员变动的情况下,解决了开发资源与测试资源分配不合理的问题。
1.4团队动员
在当前行业寒冬之下,看到“由开发者承担更多的测试工作,减少对测试资源的依赖”相关字眼,大家可能会心生疑虑,弹性研发是否意味着“压榨开发者并实行去测试化”?其实不然,下面分别从开发和测试的角度进行阐述以消除疑虑。
对于开发,根据上面的分析,弹性研发确实增加了开发者的工作量,但并未要求开发者增加工作时长,最终单位提测速率也降低了,这意味着弹性研发在增加开发者工作量的同时,也相应地拉长了开发者的开发周期,并不存在压榨开发者。只不过要求开发者采取一定措施保证研发质量,这样才有信心帮助降低测试者的测试复杂度。
测试者经历过完整的软件测试理论学习与技能培训,是软件工程中保证质量不可或缺的一环,测试者以第三者的视角审视开发者的产出,并使用其专业技能揪出隐藏的缺陷。在我们团队中,测试者正经历长时间的高强度加班,弹性研发正是为测试者减负,绝不是使用开发者取代测试者。
2.质量保证是系统性的工程
2.1概述
开发者的疑问:明明我已经做得很好了,为什么项目上线后还是出现了bug或不满足用户诉求的情况?
质量保证是系统性的工程,产品和测试分别是开发的上游和下游,从产品、到开发、再到测试,每一环缺一不可。
2.2产品
对于产品,在需求分析阶段,要求产品准确理解用户诉求,并针对用户诉求执行缜密的需求分析;在产品设计阶段,要求产品产出清晰的产品原型与完善的产品需求文档等,这些都是开发与测试在工作时的重要参考资料;尤其需要强调产品架构,它是技术架构的基础,因此要求产品在设计产品架构时具备一定的前瞻性与抽象性。
2.3开发
对于开发,必须准确理解产品需求,并根据产品架构设计科学合理的技术架构;深刻理解项目中使用的各种中间件与框架;编写的代码易于维护与阅读、符合编码规范、可测试;在提测前至少进行冒烟测试以及一定程度的正向自测。
2.4测试
对于测试,要求准确理解产品需求,根据产品需求文档对业务场景进行分析,编写科学且场景覆盖全面的测试用例,以第三者的视角审视开发者的产出并揪出隐藏的bug。
3.弹性研发团队
3.1概述
弹性研发团队主要包含两部分,弹性和右移:
Ø 右移:就是让开发者在开发工作流中尽早介入测试,这样才能尽早发现并解决问题,大大降低解决软件缺陷的成本;
Ø 弹性:是指可以根据团队内开发资源与测试资源的配比与实际交付能力动态调整转移的质量保证工作量,使提测速率与测试速率保持动态平衡,实现需求交付速率的最大化。
3.2越早发现问题,解决问题的成本就越低
谷歌经过多年实践发现,在开发工作流中,越早发现问题,解决问题的成本就越低。如下图所示,通过测试者发现问题再由开发者在测试环境中检查并解决问题就已经导致解决问题的成本一定程度上的增加;一旦在软件投产后遇到问题,解决问题的成本更是呈指数上升。
图片
横坐标从左到右是开发工作流中从早到晚的各个阶段,纵坐标是解决软件缺陷的成本
在人工测试阶段发现并解决缺陷的工作流大致如下图所示,很明显整个流程相比开发者自测更为复杂且涉及测试者与开发者之间的沟通,这些都会带来额外的成本。
图16-测试开发交互流程
3.3右移动态化
右移动态化可一定程度上自适应团队内开发资源与测试资源的配比与实际交付能力,使团队的提测速率与测试速率保持动态平衡,最终实现需求交付速率的最大化。
3.4测试即代码
弹性研发提倡测试即代码,指通过代码来表达测试,例如编写单元测试,使测试行为形成代码资产。传统的开发者自测行为,例如在IDE中进行debug、在HTML页面点击按钮调用接口,无法形成资产、无法复用、无法共享。
测试即代码要求代码是可测试的,代码难以测试说明代码职责太多、依赖关系混乱、低内聚高耦合,因此测试即代码能够促使代码模块化、高内聚低耦合、单一职责化。
使用代码表达测试后,就可以反复运行这些测试代码,每次更新代码后都可以运行这些测试代码,避免更新代码引入bug,因此测试即代码使开发者对更新代码更有信心,更利于快速迭代软件,以适应不断变化的市场环境与用户需求。
在更新代码后,一旦无法通过旧有的测试用例,就说明更新代码改变了业务逻辑,
这也就发出了提醒,是否需要更新相关的文档,促使文档与代码保持同步。
3.5测试自动化
在践行测试即代码后,就可以持续自动运行这些代码化的测试用例,例如在把代码合并至master分支时。自动化运行测试成本低、效率高,因此可频繁运行,持续对系统进行测试。
3.6建立质量文化
弹性研发提倡开发者更多地尽早地参与质量保证,提升开发者的质量意识,增强开发者的质量保证参与感,强调代码写完并不意味着开发完成。
弹性研发促使开发者编写模块化的、可测试的代码,可推动开发者使用更合理更科学的设计方案,对于建立团队的质量文化有重要意义。
3.7与测试左移的比较
传统测试左移的工作流是,测试者创建一定数量的基础测试用例,开发者开发完成后必须进行自测且通过这些测试用例之后方可提测,基本思想也是让开发者尽早参与质量保证,尽早发现并解决问题以降低解决问题的成本。
传统测试左移的模式相对固定,通过测试用例来驱动开发,有相对固定的流程,一定程度上限制了开发者的主观能动性。而弹性研发提倡开发者通过实践来发掘任何能保证研发质量的手段,坚信成功的思想或手段是会传播开的,并最终被广泛接受。
4.如何弹性右移
4.1概述
弹性研发提倡开发者更多地尽早地参与质量保证,降低测试复杂度,帮助建立质量文化,因此弹性右移不拘泥于固定的形式,只要开发者在实践弹性右移后有信心保证研发质量即可。
为了避免“当局者迷”与“思维定式”,建议由代码作者以外的第三者参与进来保证质量,机器或人工皆可,以下着重描述若干可供参考的实践方向。
4.2静态代码扫描
静态代码扫描是指使用程序分析源代码以发现潜在的问题,如bug、反模式及其他不需要运行代码就能发现的问题,例如大家熟知的SonarQube以及IDEA插件Alibaba Java Coding Guidelines。在AI大行其道的当下,也可考虑使用相关AI工具进行静态代码扫描。
4.3组内代码评审
组内代码评审是由与代码作者同组的另一名成员负责代码审核,同组成员意味着平时负责开发的业务与项目完全相同或相似,这有助于降低代码评审的门槛,同时有助于提升巴士因子。
巴士因子是软件开发中的一个术语,用于衡量软件项目的相关知识在团队成员中的共享程度。一个项目一旦有超过阈值的关键成员无法参与(例如被巴士撞伤了)就会导致项目无法推进甚至失败,这个阈值就是巴士因子。巴士因子强调项目的有关知识不应该仅被极少数人所掌握。组内代码评审促使同组至少有2名成员掌握所有业务需求与代码。
程序员由于岗位的特殊性,要求其把主要时间和精力花在编写好代码上,而不是整天与人沟通,因此组内代码评审也是一种促进组内同事学习和沟通的渠道。
目前我们团队定期进行团队范围的代码走查,这也是一种形式的代码评审,有助于形成团队的统一编码规范与风格。
4.4开发者驱动的自动化测试
►4.4.1 编写测试代码
弹性研发团队提倡测试即代码与测试自动化,因此如何编写测试代码非常关键。
好的测试代码是不变的、健壮的、明确的:
Ø 不变的测试:除非被测代码的需求改变了,否则测试代码一旦编写完成后就不再修改;
Ø 健壮的测试:在被测代码所实现的需求没变的情况下,测试代码总是能够运行通过;
Ø 清晰的测试:在测试代码运行失败的情况下,能够明确知道是哪出问题了。
(1) 编写不变且健壮的测试代码
在实践中,可以把开发者对代码的修改分为四类:
第一类是对代码进行重构:由于被测代码的需求没变,因此对代码进行重构不应该使测试代码运行失败。若测试代码运行失败,则说明重构影响了系统的行为,或测试代码的被测代码太过底层。
第二类是修复bug而修改代码:bug的存在说明现有的测试代码覆盖的用例不够全,在修复bug后,应该新增测试用例对应的测试代码,但不需要修改已有测试代码。
第三类是添加新需求而新增代码:在编写完新需求代码后,应该新增针对这部分代码的测试代码,但不需要修改已有测试代码。
第四类是因需求改变而修改代码:由于需求改变了,因此现有的测试代码肯定会运行失败,此时必须针对变更后的需求修改测试代码。
综上所述,理想情况下,只有在现有需求改变了,测试代码才会运行失败,才需要修改测试代码,这样一来维护测试代码的成本就大大降低了。
编写不变且健壮的测试代码的目标是:只要需求没有变,测试代码就始终能够运行通过,这样也就不需要修改测试代码。达到这一目标最简单最直接的测试方法就是按照用户调用系统的方式来测试系统,也就是说,针对系统的公共API进行测试,而不是系统的实现细节。
那么哪些代码才能称得上公共API呢?
仅供一个类内部使用的private辅助方法不是公共API,应该通过测试那些使用该private辅助方法的代码来测试它,这是因为private辅助方法过于底层,可能被修改的概率很大。
项目中的Service方法与通用工具类都可视为公共API,由于它对整个项目甚至所有其他项目公开,因此具备一定的抽象性与稳定性,即使内部的实现细节可能被修改,但对外呈现的行为应该是相对稳定的。
项目对外暴露的接口是公共API,由于它直接对外公开,因此具备很高的抽象性与稳定性。
(2) 编写清晰的测试代码
清晰的测试是指测试代码存在的目的以及运行失败的原因都很明确,这样在遇到测试代码运行失败时才能高效地进行修复。
下面以测试转账Service方法为例来描述何谓清晰的测试,转账Service方法有如下签名:
清晰的测试应该是完整且简洁的,完整是指测试代码包含读者理解测试的所有相关信息,简洁是指除此之外测试代码不包含其他信息,更不能包含任何逻辑,例如字符串拼接或流程控制等。
清晰的测试应该测试方法的行为而不是方法本身,应该为方法的每一种行为分别编写测试方法,这样测试方法就不会随着被测方法行为的膨胀而膨胀,也不需要修改能够运行通过的测试代码。以转账方法为例,其行为包括转账成功、因出款户余额不足导致的转账失败、因出款户不存在导致的转账失败等,因此要分别为这些行为编写测试方法。
面向行为的测试代码由三部分组成,“given”定义预设的系统环境,“when”定义对系统执行的操作,“then”验证结果。测试方法也应该由测试行为命名。
下面展示一个测试转账成功的测试方法:
最后声明一点,一旦你觉得需要额外编写测试来验证测试代码,就表明测试远远不够清晰。
►4.4.2 自动运行测试代码
可在把代码push到远程仓库时,在本地执行maven的test命令即可自动运行测试代码,也可在A-One编译构建项目时自动运行测试代码。
4.5AI自动化测试
目前我们团队有使用IDEA插件GitHub Copilot,它由OpenAI提供支持,基于数十亿行开源代码构建的模型,可实现根据代码注释编写代码、创建单元测试与静态代码扫描。这里也推荐大家使用这款插件,利用AI进行自动化测试肯定是未来的趋势,这里也提倡大家多多实践多多分享。
作者简介
张超
■服务端研发部-服务端买用技术团队-交易平台组
■ 2021年加入汽车之家,目前主要负责权益中台的建设与后端开发工作,热衷探索与分享。