得物社区项目中包括内容搜索、穿搭精选、无结果推荐、搜索debug等功能。所有功能都在一个单体应用,各种本地缓存、配置、代码交织不利于维护;当某些功能有问题之后也会影响其他接口,不利于项目稳定性。本次需求RD打算将各类功能拆分到不同项目中。
我在社区搜索迁移项目中首次落地了 Diff 自动化测试,从测试到上线的过程中,累计发现很多处bug,发现了很多潜在的、不易发现的问题;最终该项目上线后流量从5%提升到100%,仅用一周完成全量的切换,且过程中指标无异常。
什么是Diff测试
Diff测试,从字面意思上理解,就是对比测试。深入到项目中理解的话,比如某一个工程比较陈旧,代码过于冗余和复杂,维护成本较高,为了解决这些问题,会进行项目重构、框架升级、代码解耦等等。
这样的项目有个特点:纯粹的代码优化、影响范围甚广、业务上有强烈需求要保证与之前的一致性。为了验证新的代码和老的代码达到的功能和效果是一致的,采用的结果比对的方式,这样的过程我们称之为Diff测试。
新老项目迁移测试痛点
老项目的业务逻辑复杂,场景较多,梳理困难;
返回的结果数据字段也众多,甚至冗余,但属于对外协议的一部分的内容,又必须要逐个去校验,导致效率低下。
我们接下来看看传统的Diff解决方案是怎么做的,对比传统的思路,再看我是怎么做的。
传统的Diff解决方案
因为业务差异性,传统解决方案并不能在我们公司很好地落地,主要有以下两点原因:
公司当前RPC框架不支持泳道机制,需要额外搭建一套环境;而搜索的opensearch等组件外购于阿里云,额外部署的这部分成本会相对较高;
还有环境相关问题,经团队内部讨论,也找了开发RD始终无法绕过,于是只能利用开发的AB机制,使用两组用户画像一致的uid,请求同一套环境,命中不同的AB新老框架,进行结果比对。
新的Diff解决方案
经过内部讨论以及测试,我们决定用新的Diff解决方案,如下图所示:
当新的Diff方案能适应业务逻辑的时候,我们还要考虑一些其他的问题,如下:
>>流量日志解析,根据场景筛选分布
a.获取线上热词100条,遍历验证Diff结果
b.少结果、无结果推荐
c.验证热搜置顶位、运营位、人为强干预
d.接口支持的常规筛选参数
e.App版本兼容性——478、479
f.不同平台兼容性——ios、android、h5
g.翻页
h.线上实验组遍历
>>随机流量
a.线上随机抽取真实用户画像
2.字段对比的实现思路
多种对比模式:仅字段数据结构,仅判断不为空
统一格式转换:白名单额外信息字段补进
对比结果自动入库
当有bug导致不一致时,比如某个字段丢失,会大量的失败,如何确定还有无其他的bug?
黑名单过滤机制
总结与反思
诚然本次Diff发挥了很好的效果,但是Diff测试并不是完美无缺的,项目结束,和项目成员内部对齐了下,至少还有以下两个可以持续改进的点:
1.覆盖率无法完全保证,仅能从流量场景入口尽可能覆盖,但是数据场景无法构造是天然的缺陷;
2.发现问题的时机只有新老对比这一次,错过会一直存在,所以对问题归因务必要仔细;
当然这两个问题虽然完全解决比较困难,也有尽量减少影响的方案,我对Diff测试保持信心!
补充阅读-自动化工程代码实现
/**
* @author Chris
* @version 1.0
* @date 2021/10/8 11:32 上午
*/
public interface IDiffFactoryService {
/**
* 根据env环境动态获取域名
* @param path
* @return
*/
String getPath(String path);
/**
* 发送接口请求
* @param path
* @param param
* @param method
* @return
*/
JSONObject getResponseHttp(String path, Object param, String method);
/**
* 过滤部分字段,返回response
* @param jsonObject
* @param filterList
* @return
*/
JSONObject getFilterRespone(JSONObject jsonObject, List<String> filterList);
/**
* 对比结果
* @param oldJSONObject
* @param newJSONObject
* @param keyList (keyList为空,对比过滤后的字段,keyList不为空,则仅对比指定key的返回结果)
* @return
*/
AlgDiffResult getDiffResult(JSONObject oldJSONObject, JSONObject newJSONObject, List<String> keyList);
}
/**
* @author Chris
* @version 1.0
* @date 2021/10/8 11:47 上午
*/
public interface IDiffService {
/**
* 整合并实现数据的请求及结果对比
*
* @param diffParamRequest
* @return
*/
AlgDiffResult diffCompare(DiffParamRequest diffParamRequest);
/**
* 造数工具——批量生成用例
*
* @param bodyRequest
* @return
*/
List<JSONObject> getOrAddCases(BodyRequest bodyRequest);
/**
* 执行单条用例对比
* @param caseId
*/
void diffCompareCase(int caseId);
/**
* 遍历执行一个接口下的所有用例
* @param paramId
*/
void diffCompareParam(int paramId);
/**
* 遍历执行一个项目下的所有用例
* @param projectId
*/
void diffCompareProject(int projectId);
}
"query") (name =
public Iterator<Object> dataQuery() {
List<Object> listObj = ListUtil.toList();
String content = iAlgTestDataService.getById(1).getContent();
String[] strings = StrUtil.split(content, ",");
for (String str : strings) {
listObj.add(str.trim());
}
return listObj.iterator();
}
"query", testName = "热词") (dataProvider =
public void test(String data) {
ContentSearchParam contentSearchParam = new ContentSearchParam();
contentSearchParam.setQuery(data);
diffTest(contentSearchParam, ListUtil.toList("productMenu", "tags"));
}
*文:胡朝阳
得物技术
关注我们,做最潮技术人!