序言:本文主要讲解精准化测试平台在哔哩哔哩漫画技术部的演进,会介绍各个阶段需要解决的问题,解决思路、以及最终方案,并记录填过和还没填完的坑。

精准测试的背景

传统软件测试技术主要基于测试人员对业务的理解,但由于经验的局限性、被测系统的复杂性以及与真实业务数据的差距,肯定存在测试不充分的情况,所以,虽然整个测试流程很规范,但最终软件质量还是不尽如人意 。而随着分布式、微服务架构、大数据技术的出现,软件越来越复杂,迭代越来越快,测试的挑战性越来越大。测试人员急切地需要一套更加精确、高效的测试技术和方法 。精准化测试技术就在这种背景下应运而生并快速发展。

精准化测试技术是一种可追溯的软件测试技术,通过构建一套计算机测试辅助分析系统,对测试过程的活动进行监控,将采集到的监控数据进行分析,得到精准的量化数据,使用这些量化数据进行质量评价,利用这些分析数据可以促进测试过程的不断完善,形成度量及分析闭环,实现软件测试从经验型方法向技术型方法的转型

精准化测试平台演进之路

精准化测试平台的演进之路经历了3个阶段: 1.0版本静态扫描 -> 2.0版本动态链路追踪 -> 3.0流量录制回放

在一切开始之前,我们做了简单的技术调研,市面上大部分的做法,总结出来大概分为两种思路:

  1. 使用静态扫描获取代码的调用链,不同版本的代码diff,获取改动代码,然后再通过改动代码来推测测试范围。
  2. 使用动态链路追踪,来获取用例的执行路径,通过代码diff来判断影响路径,从而反向推理测试用例集。

我们考虑到方案2需要链路追踪的基建做支撑,而且在对代码和用例进行关联时,有一定的人工成本,所以第一阶段采用了方案1的思路。

1.0 版本 静态扫描

大概实现思路如下:

  1. 原始代码静态扫描,获取基础函数调用链
  2. 原数据解析,扫描结果存储至Neo4j
  3. 代码diff获取版本差异,图谱查询影响接口范围

1.代码静态扫描

由于业务代码是服务端Golang + 移动端Flutter的技术栈,所以主要针对GolangDart进行处理。

golang:

通过检索我们首先找到了 go-callvis ,它能提供代码静态扫描并通过graphviz绘制一个如下所示的可视化页面

通过调研,我们发现go-callvis能提供基础的函数调用链,但是无法采集函数节点的更多拓展信息。所以我们抽取了go-callvis底层用于采集调用链的 golang.org/x/tools 相关工具包,作为基础调用链的采集工具。

然后使用 ast 进行第二轮扫描,扫描函数节点的补充信息。

Dart:

Dart 在调研过程中并没有发现比较适用的工具。

DartDevtools 中有一个类似的功能,提供了调用树视图。

但是由于它依赖于页面的动态执行,同时并没有提供结构化数据,做存储会比较麻烦,所以该方案暂时弃用。

最终我们找到一个基于正则的perl脚本通用解决方案,然后用go重写了逻辑,作为目前的技术方案。但问题在于,因为是魔改的代码,并没有信心保证数据的绝对准确性,所以只能作为一个参考数据。而且由于前端的特性,没法保证页面的渲染正确,暂时只能将调用链采集到逻辑层,如果数据正确,我们则默认页面也是正确的。

Other:

有了基础的调用链后,服务端还需要将接口与实现的函数做映射,移动端还需要采集页面->组件->代码的映射关系。

服务端可以通过规则去做映射(spring体系通过ast能直接采集到准确的路由),移动端目前没有找到好的技术解决方案,暂时通过人工打标进行处理。

一些可能碰到的坑

  1. 项目中用的工具类,100%出自于标准库和主站封装的工具库,所以在我们目前方案,会在调用过程中剔除所有三方包和上游的依赖(默认不会出问题)。在和其他团队同学沟通的过程中,发现不少项目可能都比较依赖上游服务的稳定性,那在依赖剔除这一块可能就需要不同的处理方式。
  2. 扫描结果需要做进一步处理,例如一些项目本身的配置类、工具包,或者pb生成的文件,需要根据项目的实际情况做调整。
  3. 有一些dao层的改动,可能会导致影响范围被放大(例如用户信息级别的表),很多没有用到改动字段的接口,也会出现在测试范围内,这个时候就需要对调用链上函数的出参入参做额外的逻辑处理,以达到降噪的目的。

2. 调用链存储

提到存储,第一个想到的方案是MySql,但是因为链路存储的结构特性,最终选择了Neo4j,它在本项目中会有以下优势:

  • 扫描结果为树形结构,天然符合Neo4j的存储结构
  • 向下链路查询时,查询性能远高于传统关系型数据库
  • Neo4j内置算法:中心性算法、相似度算法等
  • 提供了可视化页面,展示目标调用链,及链上各节点的详细信息(Props)
  • 方便更多拓展功能,例如One Hot Encoding可支持GCN(图卷积神经网络)等

我们将步骤1中采集到的函数信息,直接转成对应的cypher语句,将节点(Node)关系(relationship) , 分步先后写入Neo4j中,最终得到一张类似于下方的调用链路图。

这里会有一个小细节,因为节点内容较多(关系会更多),所以在存储时肯定需要协程去并发处理。

我们按包(package)进行channel的新建,通过多个channel的通信去并发处理存储动作,当然有的同学可能发现了,如果有的核心包函数比较多,对应这个channel耗时就会比较久,可能90%其他包都写入结束了,还要等最后的1~2个包。

这就是一个典型的数据倾斜问题,其实解决方案也容易,只不过目前在我们项目中这一块的时间成本也能接受(2W节点,节点写入耗时约5min),就没有去做优化。

3. 查询影响范围

通过git开放api,我们可以在git diff内获取两次commit对比

通过文件路径与函数名,我们可以找到对应的函数节点

然后通过图谱向上追踪查询完整的调用链路,最终获取到影响的接口(客户端:组件/页面)

这里可能会面临的一些坑:

配置文件和新增文件,接口的"diff"字段是不会给任何内容的(同时gitlab有diff limit的限制),而且因为我们在平台前端提供了类似于git代码对比的功能,同时在文件树上也需要标注每个文件的新增/减少代码行,所以这里单纯的用获取diff接口的内容是不够的。需要根据对比的两个分支的last commit的详情,然后另外写逻辑进行处理。

同时因为前端的交互需要,这里也需要记录每一个“文件->代码调用链->接口”的映射关系。

到这里,获取版本改动范围的服务端逻辑就差不多结束了,接下来进入平台前端的介绍。

平台前端功能

在实际项目的使用中,如果单纯的只是提取两个版本分支(branch)做对比,然后推测试范围,在绝大部分情况下可能都不那么实用。以团队目前的情况为例,两周一次的迭代(iteration),一个迭代的改动影响接口范围,通常在30~50个,在需求偏底层改动的时候,可能扩散至上百个。

在项目实际使用中,缺乏可阅读性就意味着难以通过人力去推动,所以这一块的优化是最先需要考虑的事情。

我们可以先看一个简单的demo。

,时长00:14

这里选取的是一个需求(storie)变更导致的影响范围,可以直观的看到,一次改动所影响的接口范围。在查看某个文件的具体改动时,会展示对应的调用链和接口范围。

同时因为改动范围精确到了需求级别,通过需求管理平台,我们能知道每一个需求对应的研发、测试同学分别是谁,所以每一位同学登录到平台后,能去筛选自己所负责的需求,然后再查看需求的改动影响范围(平均每个需求改动影响接口数3~10个),这样就可大大提高结果的可阅读性。

当然,做到这种情况也对研发团队代码提交的规范性有一定要求,同时对需求管理平台有基建上的支撑。我们目前的做法是需要研发同学,对不同的需求开发写在不同的分支上,然后提测(merge)至同一个分支上。这样在结果查询时,通过对目标提测分支(target branch)的查询,才能拉到一个版本改动的所有需求,也才有了精确到需求级别后的视频中的改动范围。

2.0 版本 动态链路追踪

1.0版本结束后,能通过静态扫描获取到改动范围了,但这个时候的结果其实都是脱离业务逻辑的,而且有了测试范围还是需要测试同学进行手工测试。这个时候面临的问题就是“如何将存量的测试用例与改动范围关联上”,在减少回归测试范围的同时来确保新版本的质量,于是我们有了2.0版本,既通过动态链路追踪,来获取用例的执行路径,再通过版本改动来反推测试用例集。

大概实现思路如下:

  1. 业务代码插桩
  2. 插桩后执行业务/自动化测试用例
  3. 采集“用例-函数调用链”权重
  4. 代码diff获取版本差异,测试用例推荐

1. 业务代码插桩

在做此方案的同时,我们也考虑同步去做调用链路代码的可视化,所以考虑使用覆盖率(cover)的解决方案做二次开发。业内go语言用的比较多的覆盖率框架有 goc 和 gopher,我们在做了一定的技术调研后,因为goc支持覆盖率计数清零支持基于 Pull Request 的增量代码覆盖率等特性最终选择了goc。

goc本身提供了比较丰富的覆盖率功能,我们只需要在编译过程中,在覆盖率采集的同时,在函数级别(ast.FuncDecl)插入trace上报的逻辑。

2. 插桩后执行业务/自动化测试用例

首先精准测试平台本身要提供一个接口,将当前执行的用例与代码链路做记录,并将关联结果进行存储。

接口自动化用例因为本身就可以从response header中获取trace信息,所以可以通过api自动记录。

但是UI自动化case和手工用例就需要人工执行,在平台上通过配置页面,进行动态的执行和结果记录。

这里的采集,因为有了trace信息,所以可以比较准确地记录调用链。

如果没有trace信息,单纯通过覆盖率信息去采集也是可以的,但是需要处理非case执行影响到的代码,例如一些定时任务、回调逻辑之类的,这部分需要做额外逻辑进行剔除。

3. 采集“用例-函数调用链”权重

这一步主要是从各种维度去计算用例和函数的关联性,用于最后一步做用例推荐。有一些比较简单的方式,例如调用次数加权业务模块加权因为比较简单,就不在这里做展开。

这里主要介绍一个踩过的坑,和另一种效果比较好的做法。

坑:

我们尝试过通过文本相似度加权来计算用例与用例间的相似性,此方案对测试人员编写用例有较高的要求,如果有不同的测试人员去测试相同模块,因为书写习惯不一样,会导致case计算结果不准确。即便做了业务划分,每个同学负责不同的业务,不同版本下同一个同学写的用例可能也存在差异,所以最终弃用了此方案。

然后我们引入了图卷积神经网络(GCN),落地效果比较好,这里也举一个简单的demo为例:

  1. 通过采集不同case对函数的调用层级,构成一个C × N的稀疏矩阵 (C:测试用例个数,N:函数节点数)
  2. 将调用层级数取反,然后归一化,得到训练模型用的矩阵
  3. 根据GCN的定义X'=σ(L ?sym XW)来定义GCN层,然后堆叠两层GCN构建图卷积网络
  4. 训练完后,通过TSNE将输出层的score嵌入进行二维化处理,计算每个节点与节点的欧式距离,再存入Neo4j

4. 代码diff获取版本差异,测试用例推荐

对代码diff的处理与1.0版本的逻辑一样,可以将测试用例的推荐精确到需求级别,差异是在测试用例推荐时,测试人员可以通过筛选,将个人负责的需求用例进行聚合。

同时手工用例支持通过用例平台开放api,一键创建测试计划,并通过企微推送给对应的同学。

如果是自动化case,则是通过关联自动化测试代码的所在文件和函数信息,自动生成执行命令,结合pytest和airtest进行手动执行。

如何度量推荐结果的准确性

我们准备了这么一张图,从左到右,成本依次增高,但度量结果的信任度也是依此增高

  1. 第一个就是不进行任何人工干预,没有任何成本。通过线上遗漏缺陷数,来估量推荐结果的准确性,但问题也很明显,如果出现了bug遗漏,它的修复成本也是最高的。
  2. 第二个就是通过推荐用例执行后的代码覆盖率来进行度量,这也是业内目前采用比较多的做法,但是它需要有覆盖率的基建支撑。
  3. 第三个是统计人工检查用例与模型推荐用例,判断遗漏率。简单来讲,就是在人员充足的条件下,执行完成推荐的精准用例后,再手工执行完整的用例,通过判断缺陷是否属于推荐用例来计算推荐的准确率。这种方案成本最高,但是风险最小,同时准确率也是最可信的。

3.0 版本 流量录制回放

在2.0版本落地后,“精准化测试”的概念已经可以得到比较好的实践。静态扫描推荐用例用于冒烟测试阶段,动态链路采集推荐测试用例用于回归测试阶段。

但这时候又碰到了新的问题,就是每个版本的新用例,都需要手动执行一遍来进行链路采集,还是有比较高的人工成本。那有没有什么办法,可以彻底释放掉这一块的人力成本?既然是用于回归测试,那我们就想到了“流量录制回放”的解决方案,也就有了现在的3.0版本。

大概实现思路如下,主要是对2.0版本的用例进行了流量录制回放的功能替换:

  1. 灰度/uat环境部署插桩代码,进行流量录制
  2. 插桩代码+流量回放
  3. 采集“流量-代码覆盖率”
  4. 代码diff获取版本差异,精准筛选流量进行回放

1. 灰度/uat环境部署插桩代码,进行流量录制

首先因为插桩逻辑的侵入性,意味着这套方案并不适用于全量部署在生产环境,所以只能考虑部署在灰度或uat环境,或者使用log解析等异步的处理方式。

然后我们调研了市面上比较主流的流量录制方案,优缺点如下:

方案

优点

缺点

web服务器录制

在服务上定制化代码

请求类型比较多样

不通用,维护成本高,会占用大量线上资源

应用层录制

在网关或基于AOP切面进行录制

对代码无侵入、实现相对比较快捷简单

会占用线上部分资源、可能会对业务有影响

网络协议栈录制

直接监听网络端口,复制数据包方式录制

基本对应用无影响

比较偏向底层实现成本较高

最终因为主站已经提供了一套基于网络协议栈录制的初版解决方案,基于tshark,提供了跨语言通用的非代码侵入式的微服务流量录制方法,所以考虑直接接入,并在功能上做一些定制化开发,具体可以实现以下效果:

  • 基于单独的伴生服务,无需考虑业务方语言,跨语言通用;
  • 伴生服务的录制逻辑同时兼容http和grpc协议;
  • 无需侵入业务代码;
  • 独立的容器资源,即使出现异常,也不会影响主容器服务的正常运行

有兴趣的同学可移步至:

《B站压测实践之压测平台的演进》https://www.bilibili.com/read/cv17245121/

将tshark监听网卡录制到的指定出入端口的流量进行格式化处理,tshark录制到的流量是按请求和响应拆分的,每一条流量信息要么是一次接口请求,要么是一次接口响应,将监听到的请求先缓存在内存中,等其对应的响应到来时,拼接成完整的一次接口信息(包含请求和响应);将上述拼接完的接口信息,对其中部分可能存在敏感信息的字段进行加密处理,然后整体序列化后上传到Kafka,进行收集。

流量收集系统

采用业界通用流式数据处理方案。按如下步骤:

1. 首先Kafka集群负责接收从各个微服务实例的伴生容器发送过来的格式化后的流量数据;

2. 由Logstash服务负责从Kafka队列中去消费流量数据,并按写入ElasticSearch数据引擎;

3. ElasticSearch按微服务应用名分索引存储各应用的流量数据,供平台查询进行展示。

2. 插桩代码+流量回放

插桩动作和2.0版本一致,这里主要讲一下回放的动作。

流量回放的理想情况,是生产环境抽样进行录制,uat环境在功能测试阶段就可以回放。但是这种做法因为uat和生产环境的数据不一致问题,需要对服务端上游的所有请求、db、缓存、中间件等都能自动mock。目前yshark的功能,是可以获取并解析这些不同的tcp请求,但是对于动态mock这一块我们暂时还没有找到好的解决方案。

java在这一块已经有比较成熟的解决方案,但是go体系相对还没那么完善。目前已落地的版本,只能支持http和grpc接口的动态mock。在现有的功能下,初版只能做到uat录制+uat的回放。

3. 采集流量-代码覆盖率

其实到这里,就和api自动化执行的逻辑比较像了。我们只需要记录对应case和调用链的情况,在第4步中用于流量的筛选。

4. 代码diff获取版本差异,精准筛选流量进行回放

通过代码diff,平台会拉出所有改动涉及的函数节点,以及经过了这些函数节点的流量。

但是对于回放结果的处理,相对于传统的“流量录制回放”关注于response的结果,我们的方案可能会更多的关注于回放过程中的代码覆盖率。

这里的代码覆盖率一共有两阶段:

  1. 新功能冒烟阶段的覆盖率
  2. 流量回放的覆盖率

平台会计算函数级别的全量覆盖率,以及增量覆盖率。需求本身也有新功能,或旧功能修改的区别。理论上来讲,新需求会更侧重于阶段一的覆盖率,旧功能修改则会更侧重回放流量的覆盖率,目前我们只是单纯的做了两阶段的覆盖率合并,没有因为需求类型的差异去做细分,这也是平台未来会尝试的方向。

后续规划

功能上:

  1. 流量回放最佳实践还是生产的流量录制+uat回放,通过上游依赖的完整mock,才能完全专注于业务代码的测试,所以我们会在这方面做更多的尝试。
  2. 有了trace和覆盖率信息,可以做到uat环境的调用可视化,每一个trace经过函数节点、代码的可视化,能帮助uat环境进行问题的快速定位,并提高团队的白盒测试能力。

除了功能上的拓展,想在项目中好的落地,还需要更多交互上的细节优化。

  1. 静态扫描的结果,除了正向的范围划分,还需要考虑异常接口的提醒。我们考虑通过对需求和改动代码的特征采集,去推算可能不属于需求的改动接口,并进行推送提醒。
  2. 不同类型需求的分类,不同case覆盖率的细分,针对不同场景的统计也需要细化。
加客服微信:qmsd3699,开通VIP下载权限!