主要结论
- 详细分析最昂贵的Bug可以帮助公司节约时间、金钱和资源。
- 所收集的数据有助于对软件开发领域普遍认同的教条提出质疑。
- 在服务导向的架构中,集成测试能揭露出远多于单元测试的缺陷。
- 大部分缺陷往往集中在少量可轻松辨别的功能中。
- 简单的措施即可大幅减少最终用户可能遇到的缺陷数量。
由客户发现的软件缺陷往往是最昂贵的。很多人从事有关软件缺陷的调试(往往难以在生产环境中进行)、修复和测试工作,所有这些人需要领薪水,需要与新功能的开发工作争夺时间和资源。客户上报的缺陷往往还会让组织陷入窘境,毕竟组织内部的所有措施都没能成功发现它们。这也难怪软件维护成本通常会占到整个项目成本的40%-80%之间(根据一些研究,甚至可能高达90%:如何节约软件维护成本),而这些成本中很大一部分都用在与缺陷修复有关的工作中。修复一个Bug的准确成本很容易计算,但所造成的声誉损失往往难以衡量。客户可能因为糟糕的质量而不推荐某个应用,或弃之不用。
我们面临的情况
大部分公司并没有探究造成任何缺陷的根本原因(哪怕最昂贵的缺陷)。Komfo的做法则截然不同。我们认为缺陷是开展业务不可避免会产生的成本,从不就此产生质疑,也不会试图解决这种问题。因为我们找不到任何与客户上报的缺陷有关的行业数据来建立初始基准,我们只想知道自己所处的位置。
有这样的一个例子:我们的客户可以通过多种渠道上报缺陷:邮件、电话、社交媒体。在我们接到的所有报告中,仅12%最终对代码进行了实际的Bug修复,另外88%只是出于其他方面的原因而引起了我们的关注,也许我们的产品使用体验不够直观,或者我们的客户需要更多培训。只有12%的Bug最终修复了,这种情况是好是坏?除非其他公司也开始统计类似这样的数据,否则我们也不知道。
(点击放大图像)
不久前我读了一本名为《丰田模式(领导力篇)》(The Toyota Way to Lean Leadership)的书,书中讲到这样一个故事:丰田北美公司在机动车质保期内通过调查修复了刹车问题,将保修成本降低了60%。受到启发后我们决定采取类似的措施,于是开始收集数据来调查哪些方面存在改进的余地。
数据收集
我们的所有缺陷都记录在Jira中。根据发现时所处的不同阶段,还会为这些缺陷添加标签,例如内部发现的,或由客户上报的。我们将所有这些缺陷收集到另一个分组中,并排除所有标记为不打算修复,或已纳入考虑的缺陷。我们最关注的仅仅是缺陷本身。随后开始在Git日志中搜索相应的Jira ID(我们已经建立了一个策略,会将Jira ID保存在提交消息中)。
最终我们发现了189个缺陷以及代码库中相应的修复,这些内容的时间跨度长达两年半。对于每个缺陷,我们会收集超过40种统计信息:上报和修复时间、影响的领域、通过哪种类型的测试或技术可以提早检测到、缺陷所在方法/函数的规模和复杂度等(你可以查看我们所收集统计数据的删节版本,并将其用作自己的指导方针:goo.gl/3Gdsnm)。
数据收集过程十分漫长,因为需要收集手头的所有数据。我们已通过日常工作调查了189个缺陷,每个缺陷收集40多种统计信息,这一过程就用了超过六个月。现在我们已经可以清楚地知道需要查看哪些信息,因此冗烦的数据收集过程已经可以自动实现。
初始分析
我们得到的第一个结论是:只有10%的缺陷是我们真正感兴趣的,并且不是由开发者造成的。我们的产品是一种SaaS应用,可以从最大规模的社交网络收集各类数据(仅通过Facebook API,我们每天就要处理上千万个请求)。有时候,社交网络会不事先通知直接更改自己的API,随后我们的客户就会发现存在缺陷。对此我们只能快速反应修补自己的产品。我们会从后续分析种排除这些缺陷,因为此类问题根本无法提前解决。
(点击放大图像)
前端和后端的缺陷数量比值约为50/50,但两方面都需要密切关注。后端本身的缺陷分布就足够有趣了,大约2/3的此类缺陷位于PHP代码中,1/3位于Java代码中。我们一开始使用了PHP后端,大约一年半之前开始使用Java重写后端的部分组件。因此PHP已经用了很长时间,在我们进行缺陷调查的两年半时间里“贡献”了大部分缺陷。
(点击放大图像)
哪个编程语言造成的缺陷数量更少,这种讨论已经太多了(例如哪种编程语言通常可以产生最少量包含Bug的代码?),我们决定通过经验为自己的PHP和Java应用程序找出答案。最终发现,如果一开始就使用Java而非PHP,只有6%的缺陷是可以避免的。在PHP代码库中,有很多地方我们并不清楚变量的具体类型。对于string、date、number或object,我们会进行额外的变量检查。在Java代码库中,我们知道所有变量类型,因此无需进行额外的检查(而这也是缺陷的一个潜在来源)。
然而在将部分后端从PHP重写为Java时,仅仅6%的“Java优势”也会被进一步削弱,我们忘了包含某些函数,导致产生了缺陷。此外(关于这个结论只有一些“传闻证据”)开发者觉得相比PHP,使用Java开发的速度“更慢”。
我们两年半之前开始调查客户上报的缺陷。当时后端(这可不是双关语)完全使用PHP编写。在那一年后,我们开始用Java重写部分组件,新的后端于六个月后上线。新发现缺陷的数量并未立即减少(参阅下文Screenshot_9)。从PHP切换为Java并不意味着缺陷数量会自动减少。随后我们开始实现下文将要介绍的不同改进措施,并且需要继续等待六个月才能看到缺陷数量开始减少。重写工作也是同一批开发者进行的(我们的人员调整率很低)。
这一切意味着,根据我们自己的数据,长远来看,产品的质量将主要取决于所涉及的开发者以及所用流程。质量与所用编程语言或框架的关系其实并不大。
意外惊喜
有三件事是我们这一过程没有预料到的,它们带给我们一些意外惊喜。
(点击放大图像)
首先是,所有客户上报的缺陷中,有38%最终回归了。这意味着本来一个功能可以正常工作,我们修改了部分代码(为了修复问题或增加功能),随后用户上报称原本正常的功能无法使用了。我们并未在内部发现这样的问题。但我们知道,自己也进行了回归测试,可结论中的数据并没有那么高。这意味着我们并不具备任何类型的自动化测试可以充当检测机制,并告诉我们“你刚刚增加的那段代码可以正常工作,但会破坏某个原有功能,发布前一定要检查并修复”。通过编写自动化测试,即可将位于两个不同位置的功能逻辑“粘合”在一起。但这是一柄双刃剑。可以帮助我们更高效地捕捉回归,但同时也可能拖累前进的脚步。提交之后出现太多失败的测试会降低我们的速度,因为在继续之前还需要修复这些问题。这一过程需要权衡,但对我们而言,钟摆已经摆动到距离我们希望实现的快速开发太远的高度,因此我们需要扭转自己的方向。
(点击放大图像)
第二个惊喜是:自动化测试金字塔指南并不能帮助我们提早发现更多缺陷。客户上报的缺陷中,仅有13%可以通过编写单元测试提早发现,而与之对应的,API层面的测试可提早发现其中的36%,UI层面的测试可发现21%。菱形(大部分测试都在API层面上进行)比金字塔形更适合我们。这一问题源自我们软件的本质特征。我们的软件是SaaS应用,主要功能是通过互联网收集大量数据,并将其保存在不同数据库中以供后续分析。大部分缺陷位于不同软件的“缝隙”中。我们有超过19种不同服务,这些服务均需要不断进行网络通信。这些服务的代码位于不同代码库中,因此仅使用单元测试无法获得足够好的效果(我们考虑只在内存中运行单元测试,不涉及网络和数据库文件系统,如果有必要还会将测试数量翻倍)。同时我们认为,随着微服务和Lambda函数的崛起,相比简单的单元测试,针对完整部署的应用进行高层面的集成式测试可以更有效地发现缺陷。大部分缺陷位于不同服务地边界之间,只有针对完整部署的应用程序进行测试才能发现。在隔离的环境中(使用单元测试)对代码片段进行测试,是无法发现这些缺陷的。
(点击放大图像)
单元测试依然很有用,但并不需要对所有代码执行此类测试。我们发现只对循环复杂度(Cyclomatic complexity)不低于3的方法(72%的缺陷位于此类方法中)以及规模超过10行代码的方法(82%的缺陷位于此类方法中)进行单元测试就够了。
(点击放大图像)
第三个惊喜是:无论在内部进行何种类型的测试,都无法完全发现所有缺陷。始终有30%的缺陷只能由客户发现。为什么?边缘案例、生产环境的配置问题、非预期使用模式、不完整或错误的规范。如果有足够的时间、资金和资源,我们也可以发现那30%的缺陷,但对我们来说这样做在经济上不可行。我们面临激烈的市场竞争,需要快速前进。与其永远等待着发现那飘渺的30%,不如任其存在,但我们会尽可能通过优化提前发现它们并快速修复。当客户上报了此类缺陷并完成调查,我们会从中学习经验并进一步完善我们的系统。
举措
我们针对开发者和测试人员的流程进行了四个改动:
- 每个功能至少写一个自动化测试。如果可能,尽量专注于API测试,因为这样有助于找出更多缺陷。
- 就算最小规模的修复,也要手工进行健全性检查和直观验证。
- 要求团队领导进行代码审查。除非审查通过,否则代码不允许推送至生产环境。
- 测试过程尽可能使用边界值:过多数据、过少数据、年初和年底等,8%的缺陷都是此类值引起的。
此外在技术方面也有四个改动:
1) 每天早晨审查生产环境过去24小时的日志,在其中查找错误和异常。如果发现问题,则以高优先级修复。这样就可以尽可能早发现问题,有时客户打电话告知我们遇到问题,我们已经开始着手修复了。
(点击放大图像)
2) 我们有长时间运行的API集成测试,以前通常需要运行三小时。通过必要的改进(主要改进为:专用测试环境、测试数据生成、模拟外部服务、并行运行),相同测试现在可在三分钟内完成。我们曾受邀在2016年Google Test Automation Conference上介绍了自己的做法:渴望速度 – 将自动化测试从3小时提速至3分钟。这让我们的缺陷查找过程实现了飞跃,因为可以在每次提交后自动运行所有测试(静态代码分析、单元测试、API测试)。现在我们已经不需要每晚进行测试,也不再需要冒烟测试(Smoke test)。如果所有检查均成功通过,就可以非常自信地立即发布到生产环境。
3) 生产环境中的缺陷有时候会造成异常,而这些异常会在应用程序的日志文件中留下线索。调查过程中,我们发现相同的异常甚至早在发布到生产环境之前,就已经出现在测试环境的日志文件中了。这样来看,其实我们在真正发布到生产环境前,就有机会发现这些缺陷,但前提是对测试环境的日志进行监视。因此我们对自动化测试的执行进行了一些改动,就算所有自动化API测试均已成功通过,我们依然会在每次测试完成后检查日志文件中是否存在错误和异常。有时候因为糟糕的编码实践(例如只是记录异常信息但不进一步扩大),成功通过的测试也有可能导致内部异常但无法主动体现出来(例如主动让测试失败)。此时我们会让构建失败并调查遇到的异常。
4) 约有10%的缺陷是由未能妥善处理的非预期数据造成的:特殊字符或Unicode字符、二进制数据、出错的镜像。我们开始收集此类数据,并在自动化测试需要创建测试数据时,会使用这些内容作为“异常”数据。
收效
下图展示了我们所有举措的收效,每个柱状条对应一个季度。请留意最后四个季度,客户上报的缺陷数量持续下降。上个季度我们的缺陷数量是自从两年半前开始收集此类信息以来最少的。最后一个季度和缺陷数量最多季度之间的降幅超过了四倍。
(点击放大图像)
另外还有一件事需要注意。通常,产品的代码行数(LoC)越大,包含的缺陷就越多,但缺陷数量和代码行数之间的比例是固定不变的。在我们的情况中,虽然代码行数不断增加,但最后四个季度的缺陷数量在不断降低。
如何着手
- 调查客户上报缺陷,确定问题根源,通过修复解决问题,收集相关信息,整个过程对大部分人而言都极为冗烦。我的建议是:为缺陷调查预留专门的时间(例如每天一小时)。一旦克服了最初的不适应之后,假设你打算调查过去某天发现的缺陷,数据的收集过程将更加完善,自动化程度也会更高。然而尽管如此,我的精力依然不足以支撑每天八小时的缺陷调查。
- 确保可以将客户上报的缺陷与内部发现的缺陷区分对待。在开发修复程序时,请将缺陷的ID放在提交信息中,这样即可一一对应。
- 尽可能及时地调查缺陷 — 等的越久,需要花费的时间就越多,毕竟人的记忆很快会衰退。
- 让团队之外的某人负责调查缺陷。这个人不应与代码有任何感情联系,否则很有可能做出不够客观的判断。
- 如果要追踪太多指标,你很快会被淹没其中,因此需要明确哪些指标对组织而言是有用的,是可行的。
- 收集所有此类信息似乎需要大量工作(确实如此),但我敢保证你的每秒钟投入都是值得的。这一过程还为你提供了巨大的学习机会。
结论
调查客户上报缺陷的根源能为组织带来巨大价值。最开始的数据收集工作很不容易,但其中蕴含了大量学习的机会。让人吃惊的是有很多公司目前并没有这样做。目前的大部分组织都在争夺同一批人才,或者可以使用相同的硬件资源(AWS),你又该如何脱颖而出?为了维持客户满意度,降低成本,提高员工参与积极性,最佳的办法是“内视”,毕竟你已经有数据了。最终,所有举措都是为了实现持续不断的改进。
如果你也希望着手这样做,建议阅读下列四本书:
- “The High Velocity Edge”,作者Steven Spear
- “Antifragile”,作者Nassim Taleb
- “Toyota Kata”,作者Mike Rother
- “Moneyball”,作者Michael Lewis