持续交付2.0

数据决策:代码覆盖率在Google的状况(2019年8月)

Image

关注我,每天收获一个新技能!

下文是谷歌于 2019年8月发表的关于测试覆盖率的综述文章,来自我的笔记中的一部分,并不完整。有需要完整版本的同学可以去网络上搜索一下原文。

1

引言

代码覆盖率是衡量测试套件覆盖被测软件系统的程度。在谷歌,覆盖率信息是按每天10亿行代码计算的,包括 7 种编程语言。

让覆盖率信息具有可操作性的关键点在于:将其应用于代码变更集的覆盖率指标器和代码评审之中。

本文分析了谷歌5年内的使用率、错误率和平均代码覆盖率,并报告了从 3000 名开发人员那里收到的 512 份调查问卷,描述了这一集成,并报告了 Google 采用代码覆盖率真的情况和其所感知到的有用性。最后,本文对如何在工业环境下实现和使用代码覆盖率提出了具体的建议。


IBM 对代码覆盖率使用情况的一项大型调查显示,一旦采用了代码覆盖率,测试套件的质量就会提高;然而,易用性和可伸缩性是决定是否首先采用代码覆盖率的主要因素。

1

本文要点

可以基于现有的、已建立的库实现代码覆盖基础设施,无缝地将其集成到开发工作流中,并将其扩展到行业规模的代码库。

• 它详细介绍了谷歌的持续代码覆盖计算基础设施,以及如何可视化代码覆盖。

• 它详细说明了五年内的采用率、错误率和平均代码覆盖率。

• 它报告了代码覆盖率的有用性,分析了来自谷歌开发人员的 512 份调查问卷。

代码覆盖率是一个公认的概念,但也是一个广泛的术语,在实际应用中需要更精确的定义。本节定义了贯穿本文的重要术语和概念。

理解 Google 的开发流程及其覆盖范围的集成需要以下两个概念:

Project:是指某一项目负责者在项目定义的文件中所声明的代码集合(A project is a collection of code that the project owners declare in a project definition file. )。项目定义文件包含一组代码路径(即描述一组文件的模式)和一组测试套件以及组织元数据(如联系人电子邮件地址)(这种方式也在腾讯PCG的大运河平台中得到使用)。

ChangeList(CL):是指向 Google 代码集中版本控制系统的一次原子变更。它由一个文件列表、要对这些文件执行的操作,以及可能要修改或添加的文件内容组成。

2

谷歌工程师的开发流程

谷歌使用 单一大仓 + 主干开发 的代码分支管理模式。下图为简化后的一个工程师日常开发的工作流程。

Image图1用一个例子说明了变更列表CL的生命周期。

(1)CL1表示代码基线的开始状态。

(2)假设此时开发人员发出一个「change」命令,该命令创建一个新的变更列表 CL2。

最初,这个新创建的变更列表CL2处于「挂起」状态,此时,开发人员可以编辑其内容。

它通过所谓的“快照方式”(在图中表示为S1到S5)一次一次向前演进。

(3)当开发人员对变更列表的内容感到满意,他们就会发出一个「mail」命令启动代码评审过程。此时,自动分析(包括代码覆盖率)将启动,并通过电子邮件向评审员发送通知。变更单现在的状态被称为“评审中”。

(4) 在评审过程中,CL 的内容可能会发生变化,从而导致不同的快照(如图中S2 和S3)。

(5)当评审者与作者都同意了该 CL 时,作者将发出「submit」命令。在上面的示例中,此命令首先将CL2(S3) 与代码库的「CL4」合并为CL2(S4)。如果合并成功,则运行更多的自动测试。

(6)只有在合并成功且所有测试都通过时,才会向代码库提交变更列表。为了确保代码库中的变更列表号单调递增,提交时CL2(S4) 也被重命名为CL5。(CL2变成一个指针,任何访问它的人都将被转向到CL5。)

(7)如果「submit」命令失败,变更列表将返回到“评审中”状态,CL作者可以继续更改其内容,通常是解决合并失败或测试失败的问题。

3

代码覆盖率

Google使用它自己开发的代码覆盖基础设施来测量行覆盖率,这是一组测试执行的源代码行的百分比。请注意,空行和注释不算作代码行,因此对分母没有贡献。因为Google对所有主要语言都强制执行样式指南,行覆盖率与Google代码库中的语句覆盖率密切相关。

下图给出了每行代码所包含的语句数的分布情况。大多数行包含一个语句,而具有多个语句的行主要包含循环(初始化、条件、更新)和non-trivial, nested return。

Image

对于C++、Java和Python,谷歌的代码覆盖率也可以测量分支覆盖率,即由一组测试执行的条件语句(例如if语句)分支的百分比。分支覆盖率相当于控制流图中所有条件边的边缘覆盖率。

Google 区分了两种覆盖范围:

项目覆盖率:项目测试套件在项目代码路径上的行覆盖率。对于给定的项目,每天计算一次项目覆盖率。

变更列表CL覆盖率:受变更列表影响的所有项目的所有测试套件的行覆盖率的联合,即代码路径出现在变更列表差异中的所有项目。对于给定的变更列表,变更列表覆盖率计算一次或多次。

在变更列表覆盖范围内,区分了:

     –CL 覆盖率:变更列表中文件的所有内容的代码覆盖率。

     –CL Delta 覆盖率:仅对变更列表中修改或添加的行的代码覆盖率。(已删除的行不再存在,因此无法覆盖。)

3

单元测试

什么是单元测试? 在谷歌,仅在一台机器上(通常是在一个进程中)执行的测试用例才是单元测试。因本文中的测试覆盖率仅仅指这种单元测试用例。

有一些企业会将所有测试用例都用来计算测试覆盖率。其原因可能有两个,也可能是其中之一:

一是在引入自动化测试的早期,为了促进团队编写自动化测试的动力;

二是为了快速达到覆盖率的要求,让覆盖曲线看上去更好看。

不考虑跨多台机器的更复杂(即集成或系统)测试的代码覆盖率。其实,这一点非常重要。因为集成测试关注的是(子)系统之间的集成点,而不是每个(子)系统的内部,因此对于这些测试来说,行覆盖并不是最适合的覆盖度量。

4

基础设施

自2012年以来,就一直在积极开发的谷歌代码覆盖基础设施。此后,它不断发展,以提高兼容性并应对许多挑战。图3给出了一个高层体系结构概述,显示了基础结构的四个主要层,这些层将在后面的章节中描述。

Image

Google的代码覆盖基础设施利用成熟的代码覆盖库和内部开发的解决方案的组合来支持各种程序设计语言的覆盖计算。具体而言,基础设施使用:

●C++: gcov .

●Java: JaCoCo [5] and additional, internal support infrastructure for Android.

●Python: Coverage.py .

●Go: Go has built in coverage support (go -cover) .

●JavaScript: Istanbul [4] and an internally developed coverage library, plus significant support infrastructure built around them.

●Dart: Dart-lang has built in coverage support .

●TypeScript: Re-uses much of the JavaScript infrastructure.

为了解决集成所有这些库并统一其输出的结构挑战,外部库被修改或构建在上面。特别是,所有的覆盖库要么被修改以产生lcov格式的输出,要么被开发来将库的输出转换为lcov格式的自定义基础设施。lcov格式是gcov[7]及其前端lcov用来存储覆盖率信息的基于文本的跟踪文件格式。Google的代码覆盖基础设施使用稍微修改过的格式版本,其分支覆盖符号略有不同,不需要内部GCC id来标识分支。

在概念方面,lcov支持行和分支覆盖,但可以模拟其他类型的代码覆盖;例如,只需将每个函数的第一行标记为instrumented,就可以模拟函数覆盖。专注于行覆盖被证明是一个很好的实际选择,因为它与语句覆盖有很强的相关性,并且易于可视化和理解;

谷歌使用Blaze作为其核心构建系统。Blaze是Bazel构建系统[17]的内部版本,支持构建二进制文件、运行测试和收集代码覆盖率信息。

触发覆盖计算的一种方法是使用Blaze命令行接口为一组代码路径调用coverage命令。Blaze自动处理覆盖率计算,抽象底层结构库的实现细节。表1显示手动调用很少。例如,每天只有大约0.5%的开发人员手动调用blaze覆盖率。

Image

3.3 Coverage Automation

Image

图4说明:行号(用红色矩形突出显示)被着色以显示覆盖率信息。行号被涂成(1)绿色(如果该行被覆盖),(2)橙色(如果该行未被覆盖),(3)白色(如果该行未被检测)。注意,线条本身也是彩色的。然而,这与覆盖率无关,而是与代码更改有关。如果一行被添加到此变更列表中,则该行用不同的绿色着色;如果该行被删除,则该行用红色着色。使用颜色可视化覆盖率和代码更改信息是很棘手的。我们发现,在覆盖和代码更改方面使用绿色和红色会让开发人员感到困惑,在覆盖方面使用绿色和橙色会更好。

Google的代码覆盖基础设施在Blaze之上提供了进一步的自动化,将覆盖计算集成到常规的开发工作流中。通过在项目定义中设置单个布尔选项(enable_coverage=true),开发人员可以轻松地为项目配置自动覆盖计算。

一旦为一个项目启用了覆盖率,一个自动化系统每天为它计算一次项目覆盖率。自动化系统还计算直接更改项目中代码的所有更改列表的更改列表覆盖率。

(1)  对于每个变更列表,在第一次发送变更列表以供审阅时,至少运行一次覆盖率计算。

(2)  如果变更列表内容在审阅过程中发生更改,则在变更列表最终提交到代码库之后,将至少再运行一次覆盖率计算。

(3)  任何开发人员都可以在任何时候通过在代码评审UI中单击一个按钮来请求变更列表的覆盖率计算。实际上,变更列表的作者或审阅者在审阅过程中如果其内容发生重大更改,则会请求新的计算。

(4)  如果变更列表修改多个项目中的代码,则结果将合并。

覆盖率自动化基础设施使用覆盖率库和单个测试运行的lcov格式输出,并生成合并的覆盖率报告。不存储单独的lcov输出,只为每个文件存储两个(运行长度编码的)序列,一个编码是否检测行,另一个编码是否覆盖检测行。然后丢弃原始lcov输出。这种压缩是一种重要的资源优化,解决了一个主要的可伸缩性挑战。任何其他更详细的格式在规模上存储都会非常昂贵。在使用两个序列编码的七年中,它从未限制基础设施的可用性;可能不需要更具表现力的格式。

另一个技术挑战在于确保成功的覆盖年龄计算。为了提高成功率,项目覆盖率只根据提交的变更列表计算,Google测试自动化平台(TAP [12])已经确定所有测试都通过了这些变更列表。对于变更列表覆盖率,基础设施等待预先配置的时间量(当前为10分钟)以确定是否有任何测试失败,在这种情况下,将不会计算覆盖率。虽然这避免了在测试失败的情况下计算代码覆盖率的徒劳尝试,但并不能保证覆盖率计算成功。在启用覆盖率检测的情况下执行测试需要更多资源并更改执行环境。要解决这些差异以进一步提高成功率,需要针对项目代码或配置中的各个边角场景coner case进行数百个特定的修复。

5

可视化

Google的代码覆盖基础设施以几种不同的方式可视化覆盖信息:

1. CL coverage在Critique中可视化(它是Google的代码评审系统)。

2. 项目覆盖率在CodeSearch( https://cs.chromium.org/) 和开发人员的IDE中可视化。

3. 基础设施为项目覆盖率提供了趋势可视化,覆盖率数据可以查询以进行数据分析。

可视化:在Critique中,变更列表覆盖率显示为每个受变更影响的项目,并在摘要中报告。如图4所示,每一行的行覆盖率都是可视化的,每个文件和整个变更列表的聚合覆盖率都是用数字报告的,如图5所示。分支覆盖是为底层库支持它的语言提供的,在代码中以可视化的形式并作为聚合。Image

Image

CodeSearch的一个内部版本帮助Google开发人员导航这个包含超过10亿行代码的存储库。代码搜索使浏览文件、交叉引用符号和查看版本历史记录变得容易。它还便于绘制行覆盖,如图6所示。基础数据来自每日的项目覆盖率计算,并且仅对自上次计算以来未更改的文件可视化。

代码编辑器和IDE是可视化代码覆盖率的另一个自然选择。使用与代码搜索相同的数据源,我们为许多编辑器提供自定义支持。例如,我们的Vim元插件是开源的。

因为几乎所有处理代码的工具都有行号,所以行覆盖率是可视化的一个不错的选择。尽管覆盖集成在概念上对所有程序明语言都是相同的,但程序明语言之间存在细微的差异。例如,块的左大括号是否计入行覆盖率在不同语言之间是不同的,有时甚至在基于大括号之前的代码的单一语言中也是如此。然而,根据我们的经验,这些概念上的差异似乎并不重要:开发人员在上下文中解释可视化,而只是忽略这些小的特性。

6

数据分析

除了可视化变更列表和项目覆盖率之外,覆盖率基础设施还支持趋势可视化和数据分析。例如,开发人员可以使用预先构建的趋势可视化用户界面,在更长的时间内跟踪项目覆盖率。同样,开发人员可以使用Dremel[22]查询系统发出特定的查询。所有覆盖率数据都存储在中央数据库中,用于交互式分析。这主要是由开发人员或研究人员使用的,他们正在分析大量的覆盖率数据,类似于本文中报告的数字。

Image

6

代码覆盖率的数据来源

本节报告了分析多年历史数据和调查谷歌3000名开发者的结果。具体来说,本节展示了:

(1)  谷歌的开发人员如何采用并将代码覆盖率集成到他们的工作流程中;

(2)  代码覆盖率在谷歌是如何随着时间的推移而发展的;

(3)  代码覆盖率的感知有用性是什么。

代码覆盖在谷歌已经使用了至少18年。据我们所知,最早的自动代码覆盖基础设施的内部记录计划可以追溯到2006年。

在2012和2018之间,它收集了大约13000000个日常项目覆盖测量和14000000个变更列表覆盖测量。

7

代码覆盖率的采纳

在谷歌,代码覆盖率计算在公司级别并不是强制性的。但是,项目(或项目组)可以选择在其团队中强制使用代码覆盖率。

此外,单个团队可以选择自动代码覆盖率计算,单个开发人员可以用交互方式发起调用,进行代码覆盖率计算。

回想一下表1,它显示了开发人员手动交互调用blaze覆盖率的百分比。这方面的一个典型用例是在创建变更列表之前计算代码覆盖率。只有0.5%的开发人员每天手动调用代码覆盖率计算。相比之下,33.5%的开发人员在任何一天至少在命令行上调用一次blaze测试(即,在不计算覆盖率的情况下运行测试)。这表明开发人员在工作流程中对覆盖率和二进制通过/失败信息的处理方式不同,特别是他们检查覆盖率的频率要低得多。

对于每个变更列表,都会自动计算代码覆盖率。Google的代码审查工具将结果呈现给变更列表的作者和审查者。审阅者可以使用代码覆盖率要求作者改进更改的测试,或者作者可以主动这样做。

谷歌的测试自动化平台积极测量项目和变更列表的覆盖范围。下图显示自引入代码覆盖率自动化以来的所有数据。

2015年第一季度的增长是谷歌著名的内部“马桶测试”广告的结果。我们推测,如果当时基础设施更好的话,第一季度之后采用曲线的斜率将继续陡峭。

2015年2月是一个逐步完善和改善覆盖基础设施的时期,以允许更多的项目使用它。许多不使用代码覆盖率的项目表示感兴趣,但基础设施不支持它们的代码库(例如,导致测试失败的覆盖率检测)。

根据我们的经验(作为覆盖基础设施的所有者和维护者),不受支持的项目通常只有少数由覆盖检测引起的测试失败,但是每个项目都有不同的根本原因。

出于技术原因,我们不希望变更列表覆盖率使用率达到100%:作为资源优化,覆盖率基础结构仅在该变更列表的所有测试通过时计算变更列表覆盖率。这意味着某些特别被动的项目在给定的时间段内可能根本无法获得成功的变更列表覆盖率计算。

因此,虽然理论上最大值为100%,但实际最大值可能更低。

相比之下,项目覆盖率的使用率更有可能最终达到100%。请注意,目前还不清楚2018年第二季度变更列表覆盖率的下降情况。

7

代码覆盖率的趋势

Image

每周项目覆盖率是给定周内每日项目覆盖率的中位数。如果某个项目在给定的一周内没有成功的覆盖率计算,则该项目将从该周的聚合中排除。注意,我们排除了2015年之前的数据,因为2015年之前使用覆盖率的项目不到20%(见图7)。

上图显示了2015年至2018年期间所有项目的项目覆盖率中位数以及季度间隔(IQR)。图表显示了每周项目总覆盖率(即给定周的每日项目覆盖率中值)。每周项目覆盖率报告消除了由间歇性测试或基础设施故障引起的异常值。

Google公司并没有在整个代码库中强制任何代码覆盖率阈值。项目(或项目组)可以自由定义自己的阈值和目标。

许多项目选择使用一个集中的自愿警报系统,该系统定义了五个级别的代码覆盖率阈值。下表显示了每个级别的标准。我们怀疑,自2016年年中以来,项目覆盖范围的逐步增加反映了我们在2016年公布的这些水平的逐步采用。

当然,安全性要求较高或关键性系统有更严格的要求。为了确保高质量,这些项目使用突变测试,这提供了更严格的充分性标准。

Image

我们排除了2015年之前的数据,因为2015年之前使用覆盖率的项目不到20%(见图7)。

上图中变更列表覆盖成功率的显著下降是覆盖基础设施故障,通常在一周内解决。这些故障表明需要维护覆盖基础设施,并且应该为此类维护配置资源。

7

代码覆盖率的主观有用性

由于覆盖计算和可视化是完全自动化的,并集成到开发人员工作流中,我们希望了解开发人员在多大程度上使用所提供的数据以及代码覆盖的感知有用性。为此,我们在2019年2月进行了一项调查,包括三个利克特量表问题和一个开放式问题。图10显示了调查的屏幕截图。

Image

调查提出了以下问题:“当你是……”时,你多久会发现覆盖率有用?有三种情况(创作,审查、浏览CL)。每个场景都使用相同的5点Likert量表。我们随机选择了3000名谷歌开发人员进行了调查,其中512人(17%)做出了回应。我们从所有承诺至少一行代码的人中随机选择参与者。虽然这个集合主要包含开发人员,但它也包含非工程角色的人员。

图11显示了变更列表封面在三种不同场景中的自报有用性。

1. 在编写变更列表时,45%的受访者报告说他们“经常”或“非常经常”使用覆盖范围,25%的人“有时”使用覆盖范围。

2. 在审查变更单时,40%的受访者“经常”或“非常经常”使用覆盖范围,28%的人“有时”使用覆盖范围。10%的受访者从不使用保险。

总的来说,相当多的开发人员确实定期使用代码覆盖率并从中发现价值。

Image

来自开放式文本框的轶事证据提供了额外的见解。少数受访者不是开发人员,但仍对代码库做出更改,例如技术作者。这些受访者选择“从不”或“不确定”。在量表的另一面,一些受访者选择用“我在创作时使用覆盖率数字以确保我记住了测试”等语句来解释“非常经常”的回答。我发现,在审查时更为重要的是,很难看出边缘案件是否在没有覆盖的情况下迅速得到覆盖”。

我们分析了所有开放式文本框答案的情绪,并将其分为三类:

•正向:对代码覆盖概念总体上是正面的回答,即使它们对基础设施提出了一些问题。

•否定:对代码覆盖率概念的回答通常是否定的。

•中立:没有表达对报道的一般态度的回答。这些报告大多是关于基础设施的问题或对调查本身的评论,例如。“我怀疑我年纪太大了,不适合你的调查。我已经超过12个月没有写过或认真审查过代码了。”

表3显示了这三类答案的分布情况。60%的回答通常对覆盖率是正面的,16%是负面的,24%是中性的。一个有趣的观察是,对代码覆盖率表示负面看法的受访者比例大于不使用代码覆盖率的项目比例。图12显示,一些对覆盖率普遍表示负面情绪的受访者仍然自我报告说,他们“有时”或“很少”使用覆盖率。

7

结论

在实践中,另一个重要的技术挑战是处理失败的覆盖计算。谷歌需要多年的努力才能充分解决这些问题。使用数据和对开发人员的调查表明,开发人员通常对代码覆盖率的想法持积极态度。他们将其视为日常工作流程中的一个有价值的补充,并使用它(如果可用)。从开发人员的角度来看,实现这种积极情绪的关键是可用性和低开销。

我们希望本文描述的想法和经验能够激励和帮助其他人开发类似的基础设施。根据本文报告的经验教训,我们建议如下:

•在开发工作流程的关键点自动测量覆盖率。

•在开发人员常用的工具中显示覆盖率信息。

•期望投入精力处理由覆盖仪器引起的错误。

•依靠现有的、完善的计算覆盖率的库;这些功能足够日常使用。

•为在单一统一接口下使用的所有编程语言集成覆盖计算。

我们计划进一步调查使用数据和开发人员的意见,以便更好地了解如何使用覆盖率,以及如何更好地利用覆盖率造福于开发人员。特别是,我们将扩大调查范围,要求更多与覆盖面相关的自报数据,并将这些数据与客观行为数据相关联。最终,确定代码覆盖率的采用是否会带来更好的软件质量是很好的,但是没有一个单一的、可靠的代码质量代理度量。例如,变更列表的数量可能会受到代码质量的影响,但它也可能是一般开发或程序习惯的结果。此外,代码质量并不是使用代码覆盖率的唯一好处。

因此,我们希望在今后的工作中解决的一些开放性问题是:

* 感知有用性与实际有用性有区别吗?例如,在代码评审期间显示覆盖率是否实际上加快了评审过程?

* 感知和实际的有用性是否取决于变更单的大小?例如,我们推测,当编写完整的特性时,比如一个新类和一个测试套件,开发人员更关心代码覆盖率,而不是编写一个非常小的更改。

* 在代码复查期间显示覆盖率是否会影响最终的代码覆盖率,也就是说,如果显示覆盖率,测试添加的频率是否会高于不显示覆盖率时的频率?