有赞线上拨测系统实践(一)

前言

一直以来,做为互联网软件工程师接触最多的事务之一即是持续集成(Continuous integration,简称 CI)。持续集成俨然已成为主流互联网软件开发流程中一个重要的环节。现今有赞内部在实践持续交付(Continuous delivery,简称 CD),它能够被当作是后持续集成时代的产物。须要强调的是,不论是 CI 仍是 CD,更多的是强调做为软件开发交付过程当中的实践,而一旦交付到生产环境CI和CD就无能为力了。有赞线上拨测系统正是为了弥补这一不足。现有的线上保障手段可分为运维层面、产品层面、安全层面、服务层面和测试层面等维度。本文重点介绍咱们在测试层面的实践。前端

基于测试脚本的线上监控产生

咱们作测试线上拨测系统的初衷有如下几点:shell

  1. 主动预警线上问题。有赞有不少个业务线,各个业务线有不一样的开发测试同窗对接,咱们很难作到每次发布都把影响面评估得十分准确。运维层面的监控更多的是被动告警,即用户流量触发了线上 bug,咱们才会收到报警,用户体验不够好。咱们须要在线上 bug 预警方面变被动为主动,周期性地知晓各个业务线的健康情况。
  2. 小流量下敏捷发现线上问题。一般咱们软件的发布都是在凌晨流量很是低的时候进行。发布完成后,回归时间长(靠手动),测试面有限(没法作到次次发布全量回归)。此时须要敏捷构造一波覆盖面全的流量,在小流量背景下,敏捷发现线上问题。
  3. 知晓紧急状况下业务的受影响范围以及后续收敛状况。例如当生产环境出现网络异常等非软件故障时,须要清楚业务层面的影响;当网络恢复后,须要知道业务影响是否都已经收敛。

在此以前这些场景都须要测试人员手工介入,灵活度敏捷度都很是差。有了这套系统后,测试人员能够增长本身关注的场景,场景能够经过主动触发和定时触发来执行,经过告警系统通知到有关人员,作到第一时间排查问题,减小故障影响,下降故障时长。json

基础版

1.0 版本咱们使用通用的 SpringWeb 搭建,有赞内部称为线上机器人检查。系统结构以下c#

图片描述

1.0 版系统架构图安全

系统主要为三个模块:网络

  1. 任务调度模块。该模块将用例执行封装成系统任务,使用 Spring Quartz 来定时触发。对外提供 API 对接有赞发布平台,每当系统发布上线完成后主动触发用例执行。
  2. 测试用例模块。包括业务访问,断言和告警。测试场景须要各个业务线的测试同窗投入开发。
  3. 告警模块。对接有赞内部告警平台。

图片描述

1.0版流程图架构

系统将用例分为基础用例和场景用例,支持场景并发或者顺序同步执行。具体执行策略由用例设计者结合具体状况在用例开发过程当中设定。并发

存在的问题

基础版知足了最小可用,这种方式优势在于前期可以快速投入使用,且对于常常写集成用例的人来讲成本不高,但对其余人(测试新人、开发、运维等)则否则。归纳而言,其缺点主要集中在如下几点:框架

  1. 业务线一旦多起来,用例代码开发成本提升;
  2. 随着用例数量增长,后期用例维护成本很大;
  3. 用例上线不灵活,每次用例改动须要从新发布;
  4. 没法直观看到运行状况和业务覆盖状况;
  5. 每次执行不区分业务,全量执行;
  6. 用例代码存在冗余,效率比较低。

配置化和可视化

因为这些不可规避的问题,咱们从新设计并发布了 2.0 版本。对应解决以上问题:运维

  1. 测试用例和测试场景支持配置化,能够从管理平台上配置;
  2. 用例配置标准化,给定标准用例结构和断言策略;
  3. 经过管理平台来管理本身的用例,用例改动实时生效,无需发布;
  4. 增长前端展现,经过图表直观展现运行状况和业务覆盖状况,方便不一样人群查阅;
  5. 对接发布平台,按照指定的应用名来区分跑哪些用例;
  6. 设计用例执行框架,实现核心代码复用。

新版系统架构图以下

图片描述

2.0版系统架构图

用例模型以下:

字段 是否必填 说明
用例名称 建议命名格式:“用例类型:服务:方法”
用例类型 两种类型可选http或dubbo
用例描述 场景描述
所属业务 用例所属业务阈
请求url http协议调用的url
请求头 http header
请求参数 http或dubbo的请求入参。支持动态参数注入实现用例间依赖
服务名称 对应请求dubbo协议的接口名(包名+类名)
请求方法 http协议:GET、POST、PUT等;dubbo协议:方法名
断言 支持多个
是否开启 控制开关,关闭后再也不运行。默认开启
是否登陆 开启后,使用默认帐号进行登陆操做。默认不开启
是否重试 开启后,⽤例失败重试1次。默认否
前/后置检查 执行⽤例前/后,先执行前/后置检查,失败则中断
*此处略去了部分有赞内部使用的字段

为了更直观展现线上业务的健康情况咱们增长了丰富前端报表

图片描述

数据展现

新版本与老版本的主要区别在于:

  1. 将执行流和数据流进行了分离,测试用例设计无需编码,支持配置化,用例做为数据存放到 DB 中重复使用,用例的执行引擎管理用例的执行流。
  2. 对通用的事务进行了封装,好比登陆、切换店铺等操做,经过统一的线程池进行管理。
  3. 支持动态参数注入,实现了用例间的相互依赖,后面再单独介绍这块内容。

任务执行流程图以下:

图片描述

2.0版流程图

任务执行引擎经过不一样的工做线程实现。不一样业务用例并发执行,业务内部用例串行执行。系统根据不一样的用例的类型(http/dubbo)分发到具体任务流中。

图片描述

核心类设计

用例间依赖的实现

从用例的复杂度上讲,咱们的用例主要分为两大类:单一场景的基础用例和复杂场景的组合用例。组合用例是在基础用例的基础上进行必定的集成,用例的输入输出存在必定的依赖。咱们实现用例依赖的方式有两种:

  1. 经过配置用例的前置后置关系。
  2. 经过参数注入。

第一种方式,在配置用例的时候,给它一个前置用例,固然前置用例也是在平台中管理的。这样当执行到该用例的时候,执行引擎会先去执行前置用例。

第二种方式,针对 Json 格式的入参,咱们定义以下格式进行参数注入:

$#a,b,c#$

各个字段分别表明的含义为:

a:被依赖用例的ID
b:被依赖用例响应的字段(key值),好比:name
c:可选字段,当被依赖值位于 array 里面时,取其 index 下标
举例:{"code":"$#8,data,0#$","type":"$#10,type#$"}

参数注入的流程以下:

图片描述

参数注入流程图

断言模块设计

在新版系统里面,咱们设计了四种类型的通用断言,几乎能够知足咱们本身的全部应用场景。这四种类型分别是:
1.是否包含。
响应内容包含指定内容为 true,反之为 false。

2.非空/null。
响应内容非空/null为 true,为空/null为 false。

3.JSON 特定位置的值的“相等”判断。
这种状况系统首先会将响应内容转换成 json,添加断言时须要指定待比较对象在 json 串中的坐标。若是该坐标上的值与指定的值相等则为 true,反之为 false。
那么如何给一个 json 串的每一个值设置一个独一无二的坐标呢?考虑到 json 存在嵌套关系且 key 可能重复,咱们经过一种复合 key 的来表示这个坐标,例若有以下 json:

{

      "data": {

            "list": [

                  "1",

                  "2"

            ],

            "info": {

                  "name": "<font color=#DC143C>张三</font>",

                  "age": 18

            }

      },

      "code": 200

}

对标红的值的断言能够这样表示:{"data":{"info":{"name":"张三"}}},若是返回的位置的值为"张三"则判断结果为 true,不然为 false。

4.面向 JSON 的伪代码表达式判断

前面三种类型的断言仅知足了部分场景,对于一些复杂的断言仍然没法知足,好比上文 json 中 list size 的断言。为此,咱们引入第四种断言方式---伪代码断言。针对 list size 的断言咱们能够这样写:

<center>getJSONObject("data")getJSONObject("list").size() > 0</center>

代码在处理的时候会将该表达式拼接在 json 对象后进行执行。整段代码执行的结果为真断言为 true,不然为 false。
伪代码的动态编译、加载和调用,采用 GroovyShell 来实现。该部分代码实现以下:

public Result compare(String response) {
    Result result = new Result();

    // 单例获取GroovyShell
    GroovyShell shell = SingleGroovyUtil.getGroovyShell();

    Binding binding = null;
    JSONObject jsonObject = new JSONObject();
    JSONArray  jsonArray = new JSONArray();
    Object value = null;
    try {
        if (response.startsWith("[")){
            jsonArray = JSON.parseArray(response);
            binding = new Binding();
            binding.setVariable("data", jsonArray);
            value = InvokerHelper.createScript(shell.getClass(), binding).evaluate("data." + textStatement);
        }else {
            jsonObject = JSON.parseObject(response);
            binding = new Binding();
            binding.setVariable("data", jsonObject);
            value = InvokerHelper.createScript(shell.getClass(), binding).evaluate("data." + textStatement);
        }

        if((Boolean)value) {
            result.setSuccess(true);
        }else {
            result.setSuccess(false);
            String msg = JsonUtil.findErrMsgByJsonObject(jsonObject);
            result.setMsg(String.format("断言失败。断言的内容[%s], 错误描述[%s]", this.textStatement, msg.length()>0?msg:response));
        }

    } catch (Exception e) {
        result.setSuccess(false);
        String msg = JsonUtil.findErrMsgByJsonObject(jsonObject);
        result.setMsg(String.format("断言时发生异常。ErrMsg=[%s],actual=[%s]", e.getMessage(), msg.length()>0?msg:response));
    } finally { // 处理完后,主动将对象置为null
        binding = null;
    }
    return result;
}

插件化

新版系统知足了用例的可配置化以及可视化的要求,同时也牺牲了一部分的灵活性。例如一些复杂断言的伪代码会很是长,且可读性不高,一不留神就会出错;简单的用例依赖能够知足,复杂的用例依赖却很难知足。好比用例 A 在某些条件下依赖用例 B,其余条件下依赖用例 C,这种复杂依赖关系走配置化并不合适。基于以上考虑,咱们在现有的系统的基础上又增长了插件化的特性,来支持复杂用例的接入。

图片描述

3.0 版系统架构图

插件化的设计思想以下:

  1. 平台对外提供一套用例标准,测试同窗开发符合标准的用例添加到平台便可运行。
  2. 用例与平台彻底解耦,用例在平台可配置。
  3. 用例支持热插拔,平台无需重启。

用例标准经过接口的形式对外提供,封装成jar包暴露出来。用例设计者直接依赖该jar包并实现指定接口便可。用例接口定义以下:

public interface AbstractTestCase {

    CaseResult before();

    CaseResult run();

    void after();
}

用例开发完成后打包成 jar 包上传到平台,一个 jar 包中可包含一个用例也能够包含多个用例。

jar 包上传后平台要作的事情以下:

  1. 动态把 jar load 进 JVM
  2. 解析实现了 AbstractTestCase 接口的类
  3. 按照指定策略调用类中的方法
  4. 上报并展现结果数据

获取 jar 包中实现了 AbstractTestCase 接口的代码以下:

/**
 * 获取jar包中某接口的实现类
 */
public static List<Class<?>> getAllImplClassesByInterface(Class c) {
    List<Class<?>> filteredList = new ArrayList<Class<?>>();
    //判断是不是接口
    if (c.isInterface()) {
        try {
            //获取jar包中的全部类
            List<Class> allClass = getClassesByPackageName();
            allClass.forEach(clazz -> {
                if (c.isAssignableFrom(clazz)) {
                    if (!c.equals(clazz)) {
                        filteredList.add(clazz);
                    }
                }
            });
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    return filteredList;
}

将来

将来有赞线上拨测系统会提供更丰富的功能,例如更灵活的用例执行策略,核心用例执行频率更高,边缘业务执行频率下降;更全面的报警策略,各个业务方能够自由定制关心的用例,线上问题第一时间触达;支持多机房,目前该系统只在单机房进行部署,有赞核心业务已完成多机房部署,拨测系统也会随之调整;系统支持分布式,为了防范系统单点故障,将来还会考虑进行分布式部署。

目前这套系统能够保障测试同窗第一时间知晓有赞线上核心业务异常,未来保障的业务广度和深度会进一步提升,成为有赞线上质量保障相当重要的一环。

图片描述

相关文章
相关标签/搜索