作者简介:
- 张玲玲,来自货拉拉/技术中心/质量保障部,测试专家,负责出行领域的质量保障和效能建设工作。
- 江颖警,来自货拉拉/技术中心/质量保障部,资深测试工程师,负责出行业务服务端测试与自动化测试建设。
随着市场的变化和业务的发展,货拉拉已经从货运业务扩展到出行、国际化等新领域。为了适应业务的迅速扩张,新领域的服务通常会基于现有的系统架构进行开发。这种方法能够加快新领域服务的孵化速度,但同时也对质量保障工作提出了更高的标准和挑战:
冗余代码:废弃的业务逻辑,或者基于旧业务线系统修改的新业务线系统,都有大量冗余代码的产生,我们往往难以抽出足够的人力去清理这些冗余代码,这对基于代码覆盖率的测试监控带来了困扰;
回归成本高:业务高速发展带来了频繁发布,我们迫切需要高代码覆盖率的自动化测试代替大部分人力回归,将更多人力放在增量代码测试上;
自动化无度量手段:当前接口自动化的覆盖程度,是否能够代替全量回归等等这些都没有一个度量的手段,完全根据各业务线测试的经验来判断,这就会导致各业务线执行标准上的不一致,从而带来一些质量上的风险。
针对以上问题,我们希望打造一个智动化接口测试体系,只分析有效代码,能对各服务各接口进行代码链路分析,对接口自动化用例覆盖给出智能补充推荐,并且标准化、半自动化地完成测试脚本的编写。对自动化覆盖进度智能监控实现自动化的链路闭环。同时集成覆盖率变化统计、缺陷管理等监控能力。
自动化建设指导
自动化建设是一项庞大、长期的工程,合理区分先做哪些,后做哪些,能带来更高的收益。
结合一些服务存在较多冗余代码的背景,需要有排除冗余代码统计有效覆盖率的能力。排除冗余后再根据自动化建设推荐算法指导建设。
用例编写赋能
为了降低接口自动化编写成本,并且实现自动化用例的标准化,我们在原有框架的基础上做了很多辅助编写功能的封装,比如:
编写阶段:我们借助封装的 JsonUtils 类快速生成请求体代码、通过 PICT 类实现参数的成对自动生成、通过 MockUtils 类自动链路 mock 参数构建等等
断言阶段:各种断言校验类型均封装有工具使用,快速并且标准化的生成断言代码。不遗漏每一个中间件,不遗漏逻辑处理中每一个字段、每一个中间态
建设进展可视化
早期没有有效覆盖率监控时,曾有一个存在大量冗余代码的服务,全量覆盖率报告为 30%+,但 QA 主观认为该服务自动化测试已经完善,也无法通过全量代码报告查漏补缺。
监控是一个关键的过程,它可以为我们提供有价值的数据,以便我们做出更好的决策。
其中一些核心监控项:
下面我们详细说下核心功能的实现思路
我们是否能通过剔除冗余代码的方式来统计有效的代码覆盖率?
统计无效的代码工作量很大,那换个角度统计有效的代码是否可行?
我们在判断一个服务功能是否已经被废弃时,保险起见会查看该接口在最近几个月内是否有请求记录。反之,我们可以通过线上监控,认为一个时间段内有流量的业务是有效业务。再结合服务的代码链路关系,过滤出总的有效代码合集,基于这些有效代码生成覆盖率报告,从而解决手动排除冗余代码工作量过大的问题。
此外,再结合手动配置添加或排除指定链路代码,可以适配如无流量有效接口或无需统计接口等极端场景,增加判断的灵活性。
根据以上有效代码覆盖率检测方案,想要解决我们痛点需要具备以下几大块能力:
业务监控在各大公司已有成熟实践,如 SkyWalking 可以收集 Java 应用的 HTTP 接口和 SOA 接口的调用信息,可以收集 Kafka 消费的详细信息,包括消费的主题、分区、偏移量、消费时间等。
在货拉拉我们已经有智能监控平台 Monitor,应用监控覆盖 HTTP、SOA、Kafka 等。并且可通过开放监控数据 Open API 获取有效业务信息。基于现有的 Monitor,我们只需要调用 Open API 获取各服务一个时间段内有流量的接口、有运行记录的 job、有消费记录的 topic,便可以收拢服务的有效业务。
详见货拉拉技术文章 《核心应用覆盖率 100%,货拉拉智能监控落地实践》
代码链路解析一般实现方案为使用 Byte Code Engineering Library(BCEL)来分析 Java 字节码并生成方法调用链路。
使用 BCEL 来读取字节码文件,获取其中的类、方法和指令信息。然后分析这些信息,识别出方法调用指令,并根据这些指令生成调用图,当遇到一个方法调用指令时,使用 BCEL 获取这个指令的目标方法,然后在调用图中添加一个从当前方法到目标方法的边。通过这种方式,可以生成一个完整的调用图,显示了程序中所有方法的调用关系。
总的来说,BCEL 是实现代码链路功能的关键组件。通过使用 BCEL,可以准确地分析 Java 字节码,生成详细和准确的调用图,帮助开发者理解和分析 Java 程序的结构和行为。
在货拉拉,精准测试平台已经实现代码链路解析能力,基于代码链路解析工具二开实现支持不同类型的调用关系,生成调用图。精准测试当前货拉拉也在深入实践中,后续在精准测试实践再详细介绍。
Jacoco 官方版本并不具备根据特定代码生成报告的能力,可以通过二开 Jacoco 实现。
核心改动:
1 report 传入有效代码合集构建 CoverageBuilder 对象:
@Option(name = "--effecCode", usage = "effective code for report", metaVar = "<file>")
String effecCode;
private IBundleCoverage effecAnalyze(final ExecutionDataStore data,
final PrintWriter out) throws IOException {
CoverageBuilder builder;
builder = new CoverageBuilder(this.effecCode);
final Analyzer analyzer = new Analyzer(data, builder);
for (final File f : classfiles) {
analyzer.analyzeAll(f);
}
printNoMatchWarning(builder.getNoMatchClasses(), out);
return builder.getBundle(name);
}
2 修改 ClassProbesAdapter 类的 visitMethod 方法,判断代码是否在有效代码中:
@Override
public final MethodVisitor visitMethod(final int access, final String name,
final String desc, final String signature,
final String[] exceptions) {
final MethodProbesVisitor methodProbes;
final MethodProbesVisitor mv = cv.visitMethod(access, name, desc,
signature, exceptions);
// 有效代码覆盖率
if (mv != null
&& isContainsMethod(name, desc, CoverageBuilder.classInfos)) {
methodProbes = mv;
} else {
methodProbes = EMPTY_METHOD_PROBES_VISITOR;
}
return new MethodSanitizer(null, access, name, desc, signature,
exceptions) {
@Override
public void visitEnd() {
super.visitEnd();
LabelFlowAnalyzer.markLabels(this);
final MethodProbesAdapter probesAdapter = new MethodProbesAdapter(
methodProbes, ClassProbesAdapter.this);
if (trackFrames) {
final AnalyzerAdapter analyzer = new AnalyzerAdapter(
ClassProbesAdapter.this.name, access, name, desc,
probesAdapter);
probesAdapter.setAnalyzer(analyzer);
methodProbes.accept(this, analyzer);
} else {
methodProbes.accept(this, probesAdapter);
}
}
};
}
通过上述两步,判断代码是否是有效代码,是则生成报告,从而获得有效代码覆盖率报告。
同理,单接口纬度的覆盖率报告,只需通过单个接口名称,获取接口涉及的代码合集 report 时传入即可。
在上述需求中提到,我们期望覆盖率报告能够为自动化测试用例的补充提供指导。假设当前服务存在两个未覆盖的方法,其中方法 A 包含 50 行代码,而方法 B 只有 5 行代码。那么,编写一个针对方法 A 的自动化测试用例 a 将能覆盖更多的代码行,因此我们可以认为在抛开服务接口等其他优先级因素下,自动化测试用例 a 的优先级高于自动化测试用例 b。
当然,在实际应用中,我们仍需要结合服务的重要性、接口的重要性等其他因素,以确定哪些未覆盖的代码需要优先补充自动化测试用例
但是在一份 jacoco 覆盖率报告中,人肉方式难以筛选出哪个逻辑分支涉及的代码量多。
我们可以通过解析 Jacoco report.xml 格式的报告来获取未覆盖的方法行数信息。然后,按照未覆盖行数从多到少进行排序,通过程序筛选和排序,推送未覆盖行数更多的方法给到 QA 进行优先补充自动化 case。
xml 中方法行覆盖信息:
"method":{
"counter":[
{
"@type":"INSTRUCTION",
"@missed":"129",
"@covered":"244"
},
{
"@type":"BRANCH",
"@missed":"15",
"@covered":"15"
},
{
"@type":"LINE",
"@missed":"33",
"@covered":"62"
},
{
"@type":"COMPLEXITY",
"@missed":"11",
"@covered":"5"
},
{
"@type":"METHOD",
"@missed":"0",
"@covered":"1"
}
],
"@name":"getMemberNewList";
......
}
编写全面、有效的自动化测试用例是一项挑战。为了解决这个问题,我们提出了一种基于多因素的智能用例补充推荐算法。
这种算法考虑了服务等级、接口等级、接口流量,以及每个用例可覆盖的代码行数量等多方面因素。服务等级和接口等级反映了服务和接口的重要性,接口流量反映了接口的使用频率,而代码行覆盖数量则反映了测试用例的覆盖范围。后面我们还考虑其他因素,如代码复杂度、历史缺陷数据、代码更改频率等,以进一步提高推荐的准确性和有效性。
算法基于以上因素对每段未覆盖代码段进行评分,然后根据评分对测试用例进行排序,最后推荐评分最高的测试用例。这样,QA 团队可以优先考虑这些推荐的测试用例,从而更有效地补充和优化测试用例集。
通过使用我们的智能用例补充推荐算法,QA 团队可以更加精确地定位到需要补充的测试用例,提高测试的全面性和有效性,从而提高软件的质量和可靠性。同时,这也可以节省 QA 团队的时间和资源,提高测试的效率。
将接口自动化测试用例的编写分为 5 个阶段:
初始化测试环境 -> 构建接口请求参数 -> 发送接口请求 -> 校验程序运行结果 -> 清理测试数据
我们的辅助工具则在 5 个用例构建环节都提供了提效帮助,举 解封司机接口 unban 测试用例 的例子:
@Test(description = "封禁司机解封demo")
public void testUnban01() throws Exception {
//使用工具为 初始化测试环境 提效
//接口运行前提是有一个封禁中的司机,通过3行代码,完成(创建随机正常司机 -> 将司机状态置为封禁)
RandomDriverConstants RandomDriverConstants = new RandomDriverConstants();
randomDriverService.initData(randomDriverConstants);
driverAdapter.updateDriverStatus(randomDriverConstants.driverId, "封禁状态的枚举");
//使用工具为 构建接口请求参数 提效
//不再编写大量req类,对象,以日志中的接口请求参数为模版,只修改要改的字段
String req = jsonUtils.fileToStr("UnbanReq.json");
req = jsonUtils.reqChangeKeyOfValue("driverId", randomDriverConstants.driverId, req);
//使用工具为 发送接口请求 提效
//切换环境、调用方式、参数编码等能力配置化
Result result = requestUtil.transportPost("unban", req, Result.class);
//使用工具为 校验程序运行结果 提效
//快捷构建(校验程序运行后接口返回值DB、缓存、消息、日志是否正常)的代码
assertion.verifyEquals(result.getRet(), "0");
databaseQueryService.check("司机状态库表字段", "解封状态的枚举");
redisQueryService.check("司机的缓存", "解封的缓存数据");
kafkaReceiver.check("司机状态变更topic", "司机状态变更消息");
loggerQueryService.check("解封接口运行打印的日志", "解封日志")
//使用工具为 清理测试数据 提效
//一步清除自动化产生的所有数据
randomDriverService.clear(randomDriverConstants);
}
此外,还有许多其他用例编写提效工具,如:
CreatePICT:参数成对组合结合数据驱动(PICT:微软开源的成对组合测试工具)
//通过getPictOnQamp获取account_type和packet_type的全量组合
@DataProvider
public static Object[][] get_coupon_packet_list_packet_type() {
String content ="account_type:4,3\n" +
"packet_type:1,2,3,4,6,8\n" +
"IF [account_type] = 3 THEN [packet_type] = 3 ;";
Object[][] ob = IdUtil.getPictOnQamp(content, "2");
return ob;
}
//PICT结果数据驱动
@Test(dataProvider = "get_coupon_packet_list_packet_type", dataProviderClass = XLCouponStoneDataProvider.class, description = "获取券包列表", priority = 1)
public void test001(String account_type, String packet_type) {
xxx...
context.getInput().put("account_type", account_type);
context.getInput().put("packet_type", packet_type);
xxx...
}
MockUtils:通过 mock 手段实现异常场景如 try catch 测试
货拉拉已有基于 JVM-SandBox 的 Mock 平台,实现对 Mock 配置的增删改查 MockUtils,结合到测试用例中完成异常场景测试。
//挂载会员Mock应用
mockUtil.onlineMemberMock();
//添加会员Mock配置
Integer id = mockUtil.addMock(appId, mockClass, mockMethod, returnObj, ruleConfig);
//更新会员Mock状态
mockUtil.updateMock(id, appId, mockClass, mockMethod, returnObj, ruleConfig)
//发送请求,得到Mock异常返回
SOAResult soaResult = requestUtil.memberCenterSOAPost(path, method, req, SOAResult.class);
基于上述能力与质量管理数据,获得服务接入、覆盖率进展、缺陷等关键数据,转化为前端图表,不再赘述。
目前智动化测试方案已经在出行领域全面推广使用并且有较为显著的效果
目前展望,智动化测试仍有许多可探索的方向: