
测试专项 - 质量内建
分别从平台开发者和业务使用者角度总结一下精准测试/质量预防的一些工作
一、 平台建设
建设目标
降低由于代码变更引发的线上/线下问题数,提升研发代码质量以及测试质量
核心能力
- 影响范围精准评估 => 基于微服务的链路追踪(静态/动态)
- 测试用例精准推荐 => 测试用例与代码关联(服务级/方法级/接口级)
- 测试结果精准度量 => 单测/接口自动化/手工测试的覆盖率分析、代码扫描、技术栈、CR 指标
- 打通 CI/CD 流水线 => 为 devops 提供原子能力
业界实现
- 美团到家:代码变更风险可视化系统:基于字节码分析技术提供了诸多代码分析能力
- 滴滴:super-jacoco:基于 jacoco 的二次开发,支持增量和自定义时间的覆盖率统计,打通多环境对接。
- PerfMa:端到端精准测试解决方案:商业化产品,功能完善
核心技术
- 字节码插桩 => Jacoco 、JVM-SANDBOX
- 代码分析 => AST 、ASM、DLA
- 链路分析 => skywalking、zipkin、CAT
- 数据存储 => Hive、图数据库
Jacoco 实现原理
阶段 1. 插桩
插桩点 -> 生成插桩代码(字节码指令) -> 插入探针 -> 将插入探针后的字节码重新生成为类文件 -> 类加载
比如源代码如下:
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public int subtract(int a, int b) {
return a - b;
}
}
插桩后的代码会增加如下方法级别与行级别的探针,代码经过后会被染色。
public class Calculator {
private static boolean[] $jacocoData;
public int add(int a, int b) {
boolean[] $jacocoData = $jacocoInit();
$jacocoData[0] = true; // 方法入口探针
int result = a + b;
$jacocoData[1] = true; // 行探针
return result;
}
public int subtract(int a, int b) {
boolean[] $jacocoData = $jacocoInit();
$jacocoData[2] = true; // 方法入口探针
int result = a - b;
$jacocoData[3] = true; // 行探针
return result;
}
private static boolean[] $jacocoInit() {
if ($jacocoData == null) {
$jacocoData = new boolean[4]; // 总共4个探针
}
return $jacocoData;
}
}
阶段 2. 执行
遇到 ALOAD、xPUSH 等字节码触发探针染色
阶段 3. dump
收集探针的执行情况,每个类是一个 ExecutionData 对象
- ExecutionData:
ExecutionData
对象用于表示每个类(Class)或者源文件(Source File)的执行数据。- 包含了类或源文件的名称、探针覆盖率数据数组以及类或源文件是否被执行的标志。
- 在 JaCoCo 的 API 中,
ExecutionData
的类是org.jacoco.core.data.ExecutionData
。
public final class ExecutionData {
private final long id;
//类名
private final String name;
private final boolean[] probes;
}
- ExecutionDataStore:
ExecutionDataStore
是一个集合,用于存储所有的ExecutionData
对象。- 每个
ExecutionData
对象包含了一个或多个探针(例如,每个方法或者每行代码一个探针)的覆盖率数据。 - 在 JaCoCo 的 API 中,
ExecutionDataStore
的类是org.jacoco.core.data.ExecutionDataStore
。
- SessionInfo:
SessionInfo
用于表示单次测试运行的会话信息,包括会话名称、ID、时间戳等。- 每个会话信息都对应一次测试的覆盖率数据。
- 在 JaCoCo 的 API 中,
SessionInfo
的类是org.jacoco.core.data.SessionInfo
。
public class SessionInfo implements Comparable<SessionInfo> {
private final String id;
private final long start;
private final long dump;
}
- ExecutionDataWriter 和 ExecutionDataReader:
- 这两个类用于将
ExecutionData
对象写入文件或者从文件中读取。 ExecutionDataWriter
将ExecutionDataStore
中的数据写入文件。ExecutionDataReader
从文件中读取ExecutionData
数据并加载到ExecutionDataStore
中。- 在 JaCoCo 的 API 中,它们分别是
org.jacoco.core.data.ExecutionDataWriter
和org.jacoco.core.data.ExecutionDataReader
。
- 这两个类用于将
其他插桩方案
Jacoco 的方案已经十分成熟,基于 Jacoco 优化的各种实现也很多,但是通过字节码增强的 Jacoco 探针类型比较简单(数组),因此暂时无法基于不同标识用户的覆盖率统计。JVM-SANDBOX 比较试用与此类场景,只需在 beforeLine 与 after 事件中解析不同标识的流量并做记录类方法与代码行信息,即可实现个性化的覆盖率统计功能。简单 demo 实现如下
import com.alibaba.jvm.sandbox.api.event.BeforeEvent;
import com.alibaba.jvm.sandbox.api.event.Event;
import com.alibaba.jvm.sandbox.api.listener.EventListener;
import com.alibaba.jvm.sandbox.api.listener.ext.Advice;
import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Created by swtywang on 2024/03/10 下午3:34
*/
public class CovCollectListener implements EventListener {
public static ThreadLocal<String> localVar = new ThreadLocal<>();
private static final ConcurrentHashMap<String, CopyOnWriteArrayList<StackTraceElement>> userMethodCache = new ConcurrentHashMap<>();
private void beforeLine(Advice advice, int lineNum) {
String className = advice.getProcessTop().getBehavior().getDeclaringClass().getName();
String methodName = advice.getProcessTop().getBehavior().getName();
String user = localVar.get();
if (user != null) {
StackTraceElement stackTraceElement = new StackTraceElement(
advice.getBehavior().getDeclaringClass().getName(),
advice.getBehavior().getName(),
advice.getBehavior().getDeclaringClass().getName(),
advice.getAdvice().getLineNumber()
);
userMethodCache.computeIfAbsent(user, k -> new CopyOnWriteArrayList<>()).add(stackTraceElement);
}
}
}
二、业务实践
精准测试属于质量预防的一个子集(传统意义的精准测试通常从产生 Code Diff 开始,而从质量预防的视角看实际会更加前置)
落地场景
1. 技术方案评审
技术方案体现了 RD 对于需求的设计完备程度,也是 QA 在设计用例的一个关键参考。如果技术方案质量较差,QA&RD 通常会在各自的代码和测试用例中遗漏关键内容。技术方案缺陷也是线上事故的重要归因之一。
- 技术方案标准化(通用 + 业务特型 + badcase + ...)
2. Code Review
很多"低级代码"引起的线上问题通常会被归因为 CR 缺失,通常也只是诸如"加强 CR"等难落地的改进项,效果不佳。
Code Review 质量与研发团队的质量文化有很大的关联,需要长期的运营
从 RD 视角以及个人实际体验来看,CR 阶段很难发现问题的原因:
- (CR 缺失)对于 CR 不够重视,直接找其他 RD 或者 QA +1
- (关注点不同)CR 通常更多聚焦在代码规范和整体架构设计的是否合理
- (效果不佳)不是所有 Reviewer 都很熟悉审查的代码逻辑、CR 时间不充裕
一些落地实践:
- 制定 Code Review 规范:Code Reviewer 人选 、分支规范、评审会、代码行限制。尽可能将规范落在 git 工具中做强制性配置,跳过即红线。
- CR 度量与运营:逃逸率、CR 评论数、CR 有效率
- 代码扫描:规范问题、缺陷问题、技术债
- (美团的)Code Review 支持跳转调用链
3. 影响面分析(精准测试)
这一阶段会根据 Diff Code 生成代码变更的链路以及涉及接口/方法,帮助 QA&RD 可视化了解代码改动影响面。QA 可以根据结果补充测试用例
4. 测试覆盖率 & 风险评估报告
测试覆盖率支持统计某个测试环境的代码行覆盖率情况,指标可以直接作为准出标准。风险评估报告记录了改动链路的覆盖程度,更加全面,根据业务实际情况做消费。
5. 质量预防
定期扫描研发代码规范、缺陷以及研发各环节的指标数据,从中发现研发环节的问题与改进点。
北极星指标:千行代码缺陷率/千行当量代码缺陷率
过程指标:
- CR:有效评审率、逃逸率、评论率、应答率
- 代码规范:Critical 问题数、问题解决率、圈复杂度、注释率、重复度
- 代码缺陷:Critical 问题数、问题解决率、badcase 规则生成数
- ,,,,,,
实践思考
结合工作经历的一些想法
1. 精准测试/质量预防对业务 QA 的价值
- 精准测试本质是基于规则生成一系列客观的测试/研发过程数据供 QA&RD 消费,平台提供的很多数据还是十分有参考意义的,可以辅助我们更好地了解现状并查漏补缺。
- 质量预防重点关注研发代码质量,根据语法和历史事故提炼出核心规则 可以对增量代码做兜底的扫描;此外 CR 和技术债相关的度量指标也能更好地指导 RD/QA 做后续的改进
2. 精准测试遇到的问题
数据”过度依赖“:
- 以代码覆盖率为例,由于多个场景可能对应同一行代码,所以即使 100%的覆盖率也并不能保障没有漏测。
- 避免为了让数据指标看起来健康而消费 =>
数据难消费:
- 覆盖率数据消费需要 QA 对代码有些了解,否则又变成了主观的工具
- 链路分析数据通常大而全,如何快速精准的消费成了 QA 的难题
因为以上两点,造成了虽然现在各种开源/闭源的精准测试产品都实现了相似的功能,但是不同业务团队的使用效果却天差地别,也造成对精准测试的褒贬不一。
3. 如何使用效果更佳
- 自上而下:QA+1 与 RD+1 对齐目标后的逐层宣贯,团队所有人都会有目标感,推动会更顺畅
- 自下而上:推动过程中建立"样板间",推广最佳实践
- 度量与运营:设立北极星指标以及对应过程指标,定期分析波动情况与归因(避免过度解读,分析 topxx 问题或者业务);运营措施也会受团队规模影响
- QA 代码能力提升:覆盖率与精准链路分析的结果报告是接口/代码,懂代码的 QA 可以更好地把控结果,避免过度依赖 RD 判断
- 测试分级:比如不同地系统覆盖率阈值不同;不同的需求消费测试链路数据的要求不同