测试覆盖率 一个菜鸡的精准测试实践

Barry250 · 2021年08月07日 · 最后由 白开水pp 回复于 2023年08月09日 · 8911 次阅读

各位大佬好,我是某传统行业的一个小测试,先说一下项目情况。

  1. 我们组内人员均为功能测试。
  2. 项目时间长,没有任何的接口文档,也没有人愿意编写 swagger。

在干了一段时间之后,我发现了一个很严重的问题,回归用例过多。
我们的回归用例是,在平时的版本用例中,会有一部分 smoke 用例,把这部分用例挑出来,塞到回归用例库中。这就使得回归用例越来越多。然后又因为用例库只做增量,不怎么进行删减,而且需要覆盖多个版本,造成了每次的回归测试都很痛苦。

在某次 VCEV 沙龙,京东的熊老师建议我去看看能不能做到精准测试圈定每次回归重点范围,并推荐了我本不测的秘密,我就开始了我的精准测试之路。

我的代码水平,用我一个测试群里的大佬的话来说 “榨菜,豆腐乳,咸鸭蛋,腌黄瓜,你的代码,共通点是都很下饭。” 所以大家见笑😅

解析业务代码

首先是参考的论坛上这个帖子:
https://testerhome.com/topics/23819
先把拉到的代码编译成.class 文件,再通过 ASM 框架去进行解析,这里我碰到了我第一个难题。。。观察者模式看不懂。。。
幸好,ASM 也提供了普通的面向对象模式,感觉简单不少

FileInputStream fileInputStream = new FileInputStream(path);
ClassReader cr = new ClassReader(fileInputStream);
ClassNode cn = new ClassNode();
cr.accept(cn, 0);

然后我把他的类对象转化成了我自己的类对象,方便处理。

MyClassNodeNew myclassnode = new MyClassNodeNew();
//解析类信息
  myclassnode.setClassName(cn.name.replace("/", "."));
  List<AnnotationNode> classAnnotations = cn.visibleAnnotations;
  List<MethodNode> methodNodes = cn.methods;  //这里是获取到了类成员方法
  boolean isController = false;
  String firstPath = "";
  if(classAnnotations != null && !classAnnotations.isEmpty()) {
      for (AnnotationNode classAnnotation : classAnnotations) {
          if (classAnnotation.desc.contains("Controller")) {
              isController = true;    //为真则为Controller层类
              myclassnode.setController(true);
          }
          firstPath = getAnntationValue(classAnnotation);     //获取类的注解中的接口地址
      }
  }

然后开始转化其中的 method 方法

if (isInit(methodNode.name)) {  //这里是筛掉了init构造函数
        //新建方法对象
        MyMethodNodeNew method = new MyMethodNodeNew();
        method.setMethodName(methodNode.name);
        method.setOwner(cn.name);
        method.setControllerMethod(isController);
        myclassnode.addMethod(method);

如果当前类为 controller 层类的话,name 他的方法为接口方法,

if (isController) {       //为controller层,则获取接口地址
                    List<AnnotationNode> methodAnnotations = methodNode.visibleAnnotations;
                    if(methodAnnotations != null && !methodAnnotations.isEmpty()){
                        for (AnnotationNode methodAnnotation : methodAnnotations) {
                            String anntationvalues = getAnntationValue(methodAnnotation);
                            if (anntationvalues.length() != 0) {
                                String interfacePath = firstPath + anntationvalues;
                                //获取接口表
                                myclassnode.addInterFace(interfacePath);
                                method.setInterFaceName(interfacePath);
                            }
                        }
                    }
                }

因为公司用的 mybatis,所以我希望将表与逻辑代码也进行关联。
先去根据表信息,生成表对象,这个很好解决。
获取到 mapper.xml 里的 namespace,id 信息,将这个 xml 与和他同名的接口对象看做同一个,同时又能与表对象进行关联。

SAXReader reader = new SAXReader();
    Document doc = reader.read(path);
    Element rootelement = doc.getRootElement();
    List<Element> list = rootelement.elements();
    for (Element element : list) {
        if (element.getText().toUpperCase().contains(" " + tableName) ||
                element.getText().toUpperCase().contains(tableName + " ") ||
                element.getText().toUpperCase().contains(tableName + "\n")) {
            String functionName = element.attributeValue("id");
            String className = rootelement.attributeValue("namespace");
            String parameterType = element.attributeValue("parameterType");
            String resultType = element.attributeValue("resultType");

这里的转换其实我有一些问题,我现在罗列一下,希望有大佬指点一下。

  1. 我在处理的时候,发现我生成了大量的 getset 方法,怎么去对这些方法进行过滤?
  2. 对于通过反射实例化的类是不是就没有什么好的解决方法了?

存储

拿到了类信息和方法信息,下面是如何将其保存,留作查询呢?
特别复杂的数据库设计我也不会,毕竟下饭。。。不过我搜到了一个好东西,Neo4J,图数据库。

于是我将我整理出的类对象和方法对象转化为了类节点和方法节点,用 cql 写入 neo4j 库。
这里的逻辑是:
读取类信息 A
判断有无类 A,如果没有则创建类 A 的节点
在读取到类 A 中的方法 B 时,创建节点,并与类 A 关联
在读取到方法 B 内调用的其他类 C 的方法 D 时:
判端类 C 是否存在,如果不存在,创建类 C 的节点
判断方法 D 是否存在,如果不存在,创建方法 D 的节点
判断类 C 与方法 D 的关系是否存在,如果不存在,创建关系
创建方法 B 与方法 D 的关系

//创建类节点
private void createClassNode(Neo4jTools neo4jTools, String name){
    //检查类节点是否存在
    if(!isExistClassNode(neo4jTools, name)){
        String insertClassNode = String.format("CREATE(n:class{ClassName: \"%s\", appId: \"%s\"}) return n", name.replace("Impl", ""), APPID);
        neo4jTools.executeCQL(insertClassNode);
    };
}
  //创建方法节点
private void createMethodNode(Neo4jTools neo4jTools, MyMethodNodeNew method){
    if(!isExistMethodNode(neo4jTools, method)){
        String insertClassNode = String.format("CREATE(n:method{MethodName: \"%s\"," +
                        "isControllerMethod: \"%s\", interfaceName: \"%s\", Owner: \"%s\", appId: \"%s\"}) return n",
                method.getMethodName(), method.isControllerMethod(), method.getInterFaceName(), method.getOwner().replace("Impl", ""), APPID);
        neo4jTools.executeCQL(insertClassNode);
    }
}
   //创建关系
private void createCMShip(Neo4jTools neo4jTools, boolean isController, String className, MyMethodNodeNew methodnode){
    String classType = "class";     /// TODO: 2021/6/3  不能只是根据iscontroller来判断
    if(!isExistClassRelationship(neo4jTools, className, methodnode)){
        if(isController){
            classType = "controllerclass";
        }
        String cql = String.format(
                "match(n:%s),(m:method) where n.ClassName= \"%s\" and n.appId= \"%s\" and m.MethodName= \"%s\"" +
                        "and m.Owner= \"%s\" and m.appId= \"%s\"create(n)-[r:include]->(m)",
                classType, className, APPID, methodnode.getMethodName(), methodnode.getOwner(), APPID);
        neo4jTools.executeCQL(cql);
    }
}

最终的结果,在 neo4j 上的展示如下图:

有种瞬间爆炸的感觉,看的人密集恐惧症都来了。

筛选一下,好很多。

从最外层的接口信息,到最内层的表,调用关系基本就打通了。具体的结构可能还需要打磨,但是摇摇欲坠的大楼已经可以住人啦!~

实战

很快,我迎来了一个实战的机会,一次系统优化需要优化某些表内字段的长度,上下游统一起来。但是表太多,无法给出影响的功能。
这正好是一个很好的机会,先让开发和需规提供一版,我再自己从我的调用链路表中根据表名反推接口一波。
我比他全,大成功!😋

以下是还未实施的空想,等我做了再来更新。

与用例的关联

看了前辈们的经验,都觉得用例的维护会是精准测试的一个很大的成本,因为只能人工维护。所以我目前的想法如下:
能否根据前端代码,将增量的用例与前端的页面进行绑定?
举例:
1.根据前端代码,获取一个页面上会调用哪些接口(这个我看安卓好像可以这么玩?我不懂前端开发。。。)
2.在知识图谱中新增页面节点,将这个页面节点和有关联的后台接口做关联。
3.页面节点关联用例集。
4.这样新增的用例还是会对应到用例集里面。好处是,功能用例照写,普通测试关联功能用例和页面的关系,较为简单,服务端接口改动和前端页面关联关系自己生成。以页面为单位,颗粒度不至于太细,也不至于太粗。当然只是我的想法。

版本的对比

每次回归之前,拉取当前 sit 代码,生成新的知识图谱。
将 master 版本和当前 sit 版本的代码进行对比,找出改动的方法或者是新增的方法。
就是 jgit,这个没啥好说的。

以上就是我自己的精准测试实践,欢迎大家斧正。

共收到 29 条回复 时间 点赞
仅楼主可见
程早起 回复

你好哇,我的 QQ 是 280496355

好厉害,可惜 java 代码看不透,有没有 python 栈的大佬出来秀一波😂

仅楼主可见
Barry250 · #6 · 2021年08月09日 Author
仅楼主可见

很不错呀,麻雀虽小,五脏俱全,点赞!

针对提出的几个问题,尝试回答一下:

我在处理的时候,发现我生成了大量的 getset 方法,怎么去对这些方法进行过滤?

不知道你这里的 get set 方法,是不是 mybatis 自动生成的 entity 实体类的对应方法?如果是,遍历类的 method 进行存储的时候,应该可以过滤。

对于通过反射实例化的类是不是就没有什么好的解决方法了?

有解决方法,但要通过运行时采集调用链。可以看下这个项目:https://github.com/lastwhispers/trace-spring-boot
但注意一个点,运行时的采集,意味着如果没运行到这个链路,就会采集不到。所以只能作为补充,不好保障齐全。

另外,有个点我看文中没有提到,采集了 Controller 和 mybatis 的代码后,两者是怎么进行关联的?正常中间应该还有一个 service 层来把两者关联起来的吧,我看文中没有提及?

PS:项目时间长,没有任何的接口文档,也没有人愿意编写 swagger 这个问题,其实不用写也可以的。swagger 也认识 spring 各种注解,能自动生成包含路径、request 参数、response 格式(前提是代码里用 dto 这类实体类来写 response,而不是往 map 里加)的接口文档,只是会少了些字段说明之类的信息罢了。

陈恒捷 回复

大佬你好!!
我这里的 get set 方法是指的很多 request 类里面的 lombok 生成的。
主要是我担心根据名字过滤会过滤掉一些名字也是 getset 格式的方法

controller 层调用 service 层,会进行关联。service 层调用了 mapper 层的接口,mapper. xml 实现了这些接口,所以这里也有关联关系,这样就连接上了。

公司的测开团队有基于运行时的采集的。好像是买的第三方的,星云还是银河?

swagger 那个东西主要是,没有字段注解,没办法给功能测试的同学。。。。做不到提效。。。而且公司的测试数据管理的很严,很多数据其实不太好自动造。。。

这是把菜鸡的门槛又提高了么😂

Barry250 回复

如果是担心过滤过度,有 2 种方法:
1、也分析.java 文件,看 .java 文件是否有这些方法。如果没有,说明是编译时生成的,可以忽略。
2、分析是否符合自动生成的 get set 方法命名方式(例如 get+ 大写开头的属性名),如果符合,也忽略。

不过,这部分就算带上,我理解也问题不大?因为一般都不会改到,而且从链路完整性角度,带上这些也更完整。

陈恒捷 回复

嗯,主要是写 CQL 筛选的时候,会显得很乱,问题倒是不太大。。。

Barry250 回复

swagger 那个东西主要是,没有字段注解,没办法给功能测试的同学。。。。做不到提效。。。而且公司的测试数据管理的很严,很多数据其实不太好自动造。。。

也遇到类似问题,项目团队不是很开放,对一些工具的引入比较抵触。我们的想法是基于测试内部来去做,落到小处,逐渐去推动一些想法落地。比如 swagger,我们考虑是测试引入 yapi,在结合一些 idea 的插件,最小成本在测试内部维护接口(是不是很喜剧…),然后推一些注释规范的落地,这样对测试来讲就初步满足了提效的准入条件,围绕接口在继续做接口自动化(smoke),在基于 yapi 的能力做持续集成。有了一定效果之后在反过来去与研发团队沟通,拿案例和实践说话。
其实,很多测试的工作开展,真的需要一个好的项目经理配合,不多说,泪目

ZaZing 回复

我这边还有个问题,涉及金融,代码管控,权限什么的很严。。

学 python 的小菜鸡没看懂😂 ,有没有大佬来说说

迷龙 回复

其实问题不大,你可以用 jpype 把他转成 python 类来操作?
不过还是建议看点 java 吧。。。。你都会 python 了,再去看 java 应该也不是太难。。。

Barry250 回复

嗯,是的,要是有个那种带源码的 demo 就好了,这样上手就快了😆

neo4j 走得是我的老路,目前不做接口级别的了。
只会建议,通过反射去拿到 PackageName.ClassName.methodName 后,然后去自动生成单测和关联模块的单测,一样是三元组,但是做的是单测。

陈子昂 回复

大佬为何不做接口级别的了?

我忘了是看到菜鸡进来的,还是精准进来的😂

成功把菜的门槛提高了 😅

薄荷可乐 回复

我最近看到了 ASM 是可以获取到类的父类和实现的接口的,应该可以通过这些来进行判断

薄荷可乐 回复

还有方法的参数和参数类型,也可以拿到

有个问题,如果用 spring-data-neo4j,做一个项目的调用链记录速度非常慢,单条的方式插入,一两万节点的入库耗时直接 1 个小时以上,改成批量的插入方式,如果一个 list 里放了几千个对象,neo4j 直接夯死。。。。这完全是不能上生产的节奏呀,只能小玩小闹的节奏。

张彬彬 回复

我之前也是遇到这个情况,插了一晚上。。所以想把 setget 做个筛除。。。。再起个多线程

我试了下 saveAll 跟 save 单条的效率都很慢,saveAll 的情况如果 list 里超过 1000 条甚至有夯死的情况,目前感觉如果真的要效率快只能把链路数据存在内存了

仅楼主可见
TesterYu1 回复

B 站搜索 lsieun 大佬的视频,讲解的很详细。

另外掘金社区也有个大佬的文章有较为详细的讲解。

陈子昂 回复

时隔快 2 年,我回来看到大佬这篇回复,认知到了我以前的不足。

以前过于年少轻狂,以为解析了一个项目自己就是天,殊不知人外有人,天外有天,在短时间陆续遇到的一些项目,代码逻辑,格式,写法之五花八门,已经冲跨了我以前的自信。再次看到大佬的话,放弃莫名的接口层级的追踪,输入到函数层面,专注于输入输出,确实是当前最好的办法。

我去催饭 回复

自己实现了代码改动影响范围分析的工具,用 python 语言实现的,可以看看: https://github.com/baikaishuipp/jcci

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册