
# rtc 源码重构后代码覆盖率测试:一次实打实的实践分享
说真的,代码覆盖率测试这个话题,我在声网这边折腾了快三年了,从最初的”覆盖率能跑通就行”到现在”覆盖率必须指导代码质量”,中间走过不少弯路。最近正好团队完成了 rtc 核心模块的一次大重构,借这个机会,把覆盖率测试这块的实践经验整理一下,分享给正在做类似事情的同学们。
先说个前提:为什么重构后必须死磕覆盖率?
这个问题看起来简单,但我发现很多团队其实没想明白。代码重构本质上是在不改变功能的前提下,对代码结构开刀。原来的代码可能跑了三年都没出过问题,但重构后,你敢保证新代码和旧代码的行为完全一致吗?不敢对吧?那靠什么来验证?靠手工测试?那得测到猴年马月。
这时候代码覆盖率的价值就体现出来了。它能告诉你:重构后的代码,有没有被充分测试到?哪些分支从来没跑到过?哪些边界情况可能被忽略了?这些东西,光靠人脑是想不全的。
—
一、我们这次重构的背景和目标
先交代一下背景,方便大家理解为什么我们需要做覆盖率测试。
声网的 rtc sdk 这次重构主要涉及音视频传输的核心链路,包括抖动缓冲管理、拥塞控制算法、错误恢复机制这几个模块。代码量其实不算特别大,加起来大概三万行左右,但这些代码是真正跑在生产环境里的,每一行都直接影响通话质量。

重构的原因有几个:一是原来的代码耦合度太高,抖控算法和缓冲管理缠在一起,要改一个参数得翻好几个文件;二是历史包袱重,有些分支判断是十年前写的,注释都没有,也不知道该不该删;三是性能优化需要,原来的实现确实有些不必要的内存拷贝和锁竞争。
我们的目标很明确:重构后不仅要功能等价,性能还要提升 15% 以上,同时代码可维护性要大幅改善。在这种场景下,覆盖率测试就不是可有可无的辅助工具了,而是质量保障的核心手段。
—
二、覆盖率的几个关键指标,到底该看哪个?
说到代码覆盖率,很多同学第一反应就是”覆盖率 100%”。但实际上,覆盖率是个多维度的概念,不同指标反映的问题完全不同。
在声网内部,我们主要关注四个维度的覆盖率指标:
| 指标类型 | 含义说明 | 我们的目标值 |
| 行覆盖率 | 代码中有多少行被执行过 | ≥95% |
| 分支覆盖率 | 条件判断的每个分支是否都被覆盖 | |
| 函数覆盖率 | 有多少函数被调用过 | ≥98% |
| 路径覆盖率 | 从入口到出口的所有可能路径 | ≥70% |
这里我要说个题外话。路径覆盖率看着很诱人,100% 路径覆盖当然最好,但实现成本太高了。一个简单的 if-else-if 三层嵌套,路径数就是 2 的三次方八条。再复杂一点的业务逻辑,路径数指数级爆炸,根本不可能全部覆盖。
所以在实践中,我们会把重点放在分支覆盖率和行覆盖率上。函数覆盖率其实是最容易达标的,只要测试用例设计得当,基本都能跑到 98% 以上。路径覆盖率则是有选择性地追求,对于核心算法和错误处理路径,我们要求尽量覆盖,但也不会强求 100%。
—
三、覆盖率采集的工程实践
覆盖率的采集看似简单,就是加个编译选项然后跑测试用例。但真正要做好的话,里面有不少坑。
首先是编译环境的配置。以我们用的 GCC 为例,需要加上 –coverage 编译选项,这会插入覆盖率探测代码。但这里有个问题:探测代码本身是有性能开销的,而且会改变代码的内存布局。我们的做法是单独维护一个 coverage 分支,平时开发在主分支进行,只有在做覆盖率测试时才切换到 coverage 分支编译。这样既不影响日常开发的编译速度,也能保证覆盖率数据的准确性。
然后是测试用例的设计思路。我们把测试用例分为三类:单元测试、集成测试和混沌测试。单元测试针对每个函数单独验证,重点关注边界条件和异常分支;集成测试模拟真实的通话场景,验证多个模块之间的协作;混沌测试则随机注入各种异常情况,比如网络丢包、CPU 负载飙升、内存压力等,看看系统在压力下的表现。
这三类测试在覆盖率采集中的权重是不同的。单元测试贡献最稳定的覆盖率数据,因为它的执行路径最确定;混沌测试虽然覆盖率高,但覆盖的路径往往集中在常见场景,真正容易被忽略的反而是集成测试中那些”应该不会出问题”的地方。
还有一个点值得单独说:覆盖率数据的收集频率。我们的做法是每日构建时会自动跑一遍覆盖率测试,生成报告并和前一天的数据对比。如果覆盖率下降了 1% 以上,就会自动发告警邮件。这个机制帮我们发现过好几次”测试用例被不小心删掉”或者”新代码没有对应的测试覆盖”的情况。
—
四、重构过程中的覆盖率变化曲线
这是一个很有意思的话题。代码重构不是一蹴而就的,往往需要几周甚至几个月的时间。在这个过程中,覆盖率数据的变化其实能反映出很多问题。
在我们这个项目里,覆盖率曲线大致经历了三个阶段。第一阶段是重构初期,覆盖率会明显下降,有时候一天能掉五六个点。这是因为新代码刚写好,测试用例还没跟上,有些分支根本没人测。这个阶段我们不会太焦虑,但会密切监控哪些模块掉得最厉害,优先补齐测试用例。
第二阶段是重构中期,覆盖率开始缓慢回升。这时候测试用例逐渐补全,新的代码结构也开始发挥优势,某些以前很难测到的分支现在变得容易测试了。我记得有个老代码里的三层嵌套 if,条件组合特别多,原来基本没法写单元测试。重构后我们把它拆成了策略模式,每个策略独立测试,覆盖率直接从 60% 提到了 90% 以上。
第三阶段是重构后期,覆盖率进入平台期。这时候再想提升,每提升一个百分点都需要投入更多的测试资源。这时候我们就会做一次全面的覆盖率分析,看看那些还没覆盖的分支到底是什么情况。有些是可以接受的,比如某些错误分支只在极端条件下才会触发,生产环境中几乎不可能遇到;有些则必须补齐测试,比如核心算法中的边界处理逻辑。
—
五、几个典型的坑和解决方案
在做覆盖率测试的过程中,我们踩过不少坑,挑几个印象最深的分享一下。
第一个坑是宏定义导致的覆盖率失真。有些代码里用宏定义来控制编译开关,不同的配置会激活不同的代码块。如果我们只用默认配置去跑覆盖率测试,那些被 #ifdef 隔离的代码就永远不会被覆盖到。解决方案是准备多套编译配置,分别在不同配置下跑覆盖率测试,最后汇总报告。这个方法虽然麻烦,但能保证宏定义覆盖率的完整性。
第二个坑是多线程代码的覆盖率采集。多线程程序的执行路径是不确定的,同一个测试用例这次可能跑这条路径,下次就跑那条路径了。这导致覆盖率数据抖动很大,有时候同一天跑两遍覆盖率结果能差三四个点。我们的做法是增加测试的执行次数,同一个场景跑十遍取并集。虽然这样会延长测试时间,但至少能保证覆盖率数据的稳定性。
第三个坑是第三方库的覆盖率和自研代码混在一起。RTC 系统肯定要用到不少第三方库,比如音视频编解码库、网络库什么的。如果覆盖率报告里包含第三方库的覆盖率数据,一方面数据量太大看不过来,另一方面第三方库我们也没办法修改。解决方案是在编译选项里明确指定 coverage 只需要覆盖自研代码的目录,第三方库自动排除。
—
六、覆盖率数据的质量分析和改进
覆盖率数字只是一个结果,更重要的是分析覆盖率数据背后的质量信息。我们总结了几个需要重点关注的分析维度。
未覆盖分支的原因分析。每个未覆盖的分支都要追问:是测试用例设计遗漏,还是代码逻辑本身有问题?比如有个判断分支是 if (ptr != NULL),但测试用例从来没传过 NULL 指针,所以这个分支一直没覆盖到。这时候就要考虑是否补一个测试用例,或者这个 NULL 判断本身是不是冗余的。
高频覆盖路径的性能热点。覆盖率数据还能帮助发现性能热点。那些被高频覆盖的代码路径,往往是系统性能的瓶颈所在。我们在一次分析中发现,抖动缓冲区的入队操作覆盖率高达 99%,但这个操作在某些场景下耗时异常的高。顺着这个线索,我们优化了队列的实现,性能提升了 12%。
长期未变化的覆盖率区域。如果某个模块的覆盖率连续几周都没有变化,要么说明这个模块已经很稳定了,要么说明大家都在回避它。我们的做法是每季度做一次全量覆盖率审计,把长期”冻结”的区域翻出来重新审视。有些代码确实是稳定不需要改的,有些则可能是测试用例老化需要更新。
—
七、对团队实践的几点建议
说了这么多,最后给准备在团队内推行覆盖率测试的同学几点建议。
覆盖率测试应该是渐进式的,别一开始就追求 100%。先从核心模块开始,建立起采集和分析的流程,再逐步扩展到全量代码。如果一开始就铺得太开,很容易变成”为了覆盖率而刷数字”,反而失去了质量保障的意义。
覆盖率数据要和服务质量指标联动。覆盖率再高,如果线上崩溃率没改善,那说明测试用例本身有问题。我们会把覆盖率数据和线上的异常率、用户投诉率放在一起看,形成一个完整的数据闭环。
最后也是最重要的一点:覆盖率是手段不是目的。真正重要的是代码质量和用户体验。覆盖率 100% 但全是无效测试,不如覆盖率 80% 但每个测试都真正验证了关键逻辑。这个心态要摆正。
好了,这就是我们在声网做 rtc 源码重构后覆盖率测试的一些实践和思考。写得比较零散,但都是实打实踩出来的经验,希望能给各位带来一点参考价值。
