Apifox 持续集成 + Java + 企微机器人 + xxljob 定时推送

Apifox 持续集成 + Java + 企微机器人 + xxljob 定时推送

—— 感谢  Apifox 用户「七分红酒」的投稿

如何定时执行 Apifox  API 接口测试后,自动将报告和数据推送到企业微信群中?

本文主要实现把 Apifox 测试套件,通过 apifox-cli 持续集成执行后,生成 json 数据报告和 html 测报告,本地解析 json 拿到有用的数据,和 html 报告一起,通过定时执行推送到企微群里

效果见下图:

一、需要的安装

1、安装 Node.js

yum install -y nodejs 

2、安装 npm

yum install -y npm 

3、安装 Apifox  CLI

npm install -g apifox-cli

Apifox  CLI 是  Apifox  的命令行运行工具,主要用来做持续集成。支持实时运行在线数据和导出数据运行 2 种方式。因为后续套件可能去增加用例,所以我们选择使用在线数据进行调用。关于 Apifox-cli 的详细介绍可以参考官方文档 —— Apifox  CLI 介绍

二、本地测试运行生成本地报告

1、必要的安装后,可以获取测试用例或套件的执行链接,先本地测试一下。 执行链接的获取方式:测试套件或用例点击详情,点击持续集后 -> 生成命令 -> 然后复制后 -> 在终端命令行直接运行在线数据。 命令类似长这样 :  

apifox run http://xxx/api/v1/api-test/ci-config/xxxx/detail?token=xxxx -r html,cli 

三、了解 Apifox 执行命令的伴随命令

我只列本次可能需要用到的,其它的可以这里去找更多选项 —— Apifox 执行命令扩展执行

1、通过-r 指定生成 json 格式或 html 格式。生成 json 格式时,可以生成到本地,获取到后,解析拿到自己想要的数据;生成 html 格式时,可以推送出去。也可以使用 -r json,html,直接生成对应的 json 和 html 文件。

 -r, --reporters [reporters]           指定测试报告类型, 支持 cli,html,json (default: ["cli"])

2、通过 --out-dir 指定输出报告的位置,如果不指定,默认会在当前路径下新建apifox-reports 文件夹。 自己也可以指定生成到自己想要的目录下。我的生成思路是把文件指定存到 /tmpapifox-reports,然后每次解析使用后,删除 2 个文件。

 --out-dir <outDir>                    输出测试报告目录,默认为当前目录下的 ./apifox-reports

3、通过--out-file 可以更改生成的 json文件或 html 文件的名称,也可以不自定义。我本次没有自定义。

--out-file <outFile>                  输出测试报告文件名,不需要添加后缀,默认格式为 apifox-report-{当前时间戳} 

4、--database-connection 指定你的数据库配置文件访问路径,如果你的测试套件用到的用例中,存在数据库校验;就需要在执行的当前路径配置数据库依赖配置文件。如果没有数据库校验则忽略。下载位置见下图:

--database-connection <path>          指定 [数据库配置] 的所处文件路径,使用 URL 测试的时候必须指定 

四、代码部分-1 :终端执行生成报告

1、先找到可以运行终端命令的代码,自己写或找个工具类都可。主要依赖方法是 process = Runtime.getRuntime().exec(command); 这是我在网上找的。懒得找的话,创建个工具类把下边这个方法实现贴进去可以运行。

// command为运行命令,waitTime为命令执行等待时间,单位=秒。
public static String run(String command,int waitTime) throws IOException {
    Scanner input = null;
 String result = "";
 Process process = null;
 try {
        process = Runtime.getRuntime().exec(command);
 try {
            // 等待命令执行完成
 process.waitFor(waitTime, TimeUnit.SECONDS);
 } catch (InterruptedException e) {
            e.printStackTrace();
 logger.info("CmdUtil- run(String,int)遇到异常1");
 }
        InputStream is = process.getInputStream();
 input = new Scanner(is);
 while (input.hasNextLine()) {
            result += input.nextLine() + "\n";
 }
        result = command + "\n" + result; //加上命令本身,打印出来
 } catch (Exception e){
        e.printStackTrace();
 logger.info("CmdUtil- run(String,int)遇到异常2");
 }
    finally {
        System.out.println("---finally---");
 if (input != null) {
            input.close();
 }
        if (process != null) {
            process.destroy();
 }
    }
    return result;
}

2、通过调用终端类执行,生成对应的 json 报告和 html 报告。通过加指令指定生成的报告格式指定使用的 db.json 位置为 database-connections.json (我提前放过去的),指定生成的路径 -out-dir /tmp/apifox-reports

//生成json和html报告的命令
String apifoxcmd = "apifox run https://api.apifox.cn/api/v1/api-test/ci-config/359896/detail?token=xlH8c24skOlIwGWN08O-d2 -r json,html,cli --database-connection /tmp/database-connections.json  --out-dir /tmp/apifox-reports"
 //生成报告
CmdUtil.run(apifoxjsoncmd,10);

五、代码部分-2: 到 json 和 html 报告并解析 json 报告

先拿到 json 报告解析报告字段,拿到想要的字段。例如套件名称、总请求数、失败数、失败接口的名称。 字段有很多,想要什么,就自己解析拿到就行。

为了简化拿文件的方式,我是每次新生成报告文件,读取和解析后,就删除文件。这样每次我生成后文件夹下只有 1 个 json 文件和 1 个 html 文件,读完就删除。

//报告文件夹
 File reports = new File("/tmp/apifox-reports");
 //json报告路径
 String jsonReportPath = "";
  //html报告路径
 String htmlReportPath = "";

//一共就2个文件,没有走循环,直接拿值。不是[0]就是[1]
  if(reports.listFiles()[0].getAbsolutePath().contains("html")){
      jsonReportPath = reports.listFiles()[1].getAbsolutePath();
      htmlReportPath = reports.listFiles()[0].getAbsolutePath();
  }else{
      jsonReportPath = reports.listFiles()[0].getAbsolutePath();
      htmlReportPath = reports.listFiles()[1].getAbsolutePath();
  }
  //json文件
  File jsonReport = new File(jsonReportPath);
  //html文件
  File htmlReport = new File(htmlReportPath);
  logger.info("jsonReportPath = "+ jsonReportPath);
  logger.info("htmlReportPath = "+ htmlReportPath);

//读取到文件json内容
String jsonString = new String(Files.readAllBytes(Paths.get(jsonReportPath)));
JSONObject reportObject= JSONObject.parseObject(jsonString);

//测试套件名称
String TJName = reportObject.getJSONObject("collection").getJSONObject("info").getString("name");
logger.info("测试套件名称 = "+ TJName);

//总请求数
String requestTotal = reportObject.getJSONObject("result").getJSONObject("stats").getJSONObject("requests").getString("total");
logger.info("总请求数 = "+ requestTotal);

//总断言数
String assertionsTotal = reportObject.getJSONObject("result").getJSONObject("stats").getJSONObject("assertions").getString("total");
logger.info("总断言总数 = "+ assertionsTotal);

//请求失败数
String requestFailed = reportObject.getJSONObject("result").getJSONObject("stats").getJSONObject("requests").getString("failed");
logger.info("请求失败数 = "+ requestFailed);

//请求成功率
float requestOk = (Float.parseFloat(requestTotal)-Float.parseFloat(requestFailed))/Float.parseFloat(requestTotal);
String  requestOkPersent =  String.format("%.2f%%",requestOk*100);
logger.info("请求成功率 = "+ requestOkPersent );

//响应平均时间
String responseAverage = reportObject.getJSONObject("result").getJSONObject("timings").getString("responseAverage");
logger.info("响应平均时间 = "+ responseAverage);

//报错用例名列表
List<String> httpApiNames = new ArrayList<>();
JSONArray failures =  reportObject.getJSONObject("result").getJSONArray("failures");
for(int i = 0;i<failures.size();i++){
    String httpApiName = failures.getJSONObject(i).getJSONObject("source").getJSONObject("metaInfo").getString("httpApiName");
    if(!httpApiNames.contains(httpApiName)){
        httpApiNames.add(httpApiName);
    }
}
//删除json文件
logger.info("执行删除json文件 ----"+jsonReportPath + "---" + jsonReport.delete());

 六、代码部分-3 :通过企微机器人把 json 解析内容发到群里

//消息推送
String content = "Apifox测试套件《"+TJName+"》执行报告\n ";
content = content.concat("总接口数【"+requestTotal+"】 \n ");
content = content.concat("接口总断言数【"+assertionsTotal+"】 \n");
content = content.concat("接口请求失败数【"+requestFailed+"】 \n");
content = content.concat("接口请求成功率【"+requestOkPersent+"】 \n");
if(httpApiNames.size()>0){
 logger.info("本次执行所有报错的数据 = "+ httpApiNames);
 content = content.concat("接口失败名明细【"+httpApiNames.toString()+"】 \n");

}else{
 logger.info("本次执行无报错");
}

String now = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss").format(new Date());
content = content.concat("【"+now+"】 \n");

Map<String,Object> markdownMap = new HashMap<>();
markdownMap.put("content",content);

Map<String,Object> text = new HashMap<>();
text.put("content",content);

//如果有报错
if(Integer.parseInt(requestFailed)>0){
 ArrayList<String> mentioned_list = new ArrayList<>();
//           mentioned_list.add("需要提醒的某个人");
//            mentioned_list.add("@all"); //需要提醒所有人
 		text.put("mentioned_list",mentioned_list);
}

Map<String,Object> r_params = new HashMap<>();
r_params.put("msgtype","text");
r_params.put("text",text);

Map<String,String> headers = new HashMap<>();
headers.put("Content-Type", "application/json");

ResponseObj responseObj = TCHttpUtil.sendPostWithObjMap("https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx",headers,r_params);

System.out.println("机器人响应结果:"+ responseObj.getResult());

七、代码部分-4 :通过企微机器人把 html 报告发送到群里

这里需要 2 步,第一步,html 文件需要先通过企微机器人 api 上传到去,会返回一个 media_id。这里注意是上传文件,需要 type=formdata 后推文件。第二步,根据企微机器人的推送消息方法,把 media_id 推上去,html 文件就可以推送到群里了。

企微机器人的方法,不再赘述,可以去官网这里去查看:企微机器人Api文档

//html报告上传(html报告已经在前边的步骤拿到)
 HttpHeaders requestHeaders = new HttpHeaders();
 requestHeaders.setContentType(MediaType.MULTIPART_FORM_DATA);
 MultiValueMap<String, Object> requestBody = new LinkedMultiValueMap<>();
 FileSystemResource resource = new FileSystemResource(htmlReport);
 requestBody.add("upload_file", resource);
 // 发送上传请求
 HttpEntity<MultiValueMap> requestEntity = new HttpEntity<MultiValueMap>(requestBody, requestHeaders);
 ResponseEntity<HashMap> responseEntity = new RestTemplate().postForEntity("https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media?type=file&key=xxx",
         requestEntity, HashMap.class);
 logger.info("企微机器人上传文件成功 = "+ responseEntity.getBody());
 String media_id = (String)responseEntity.getBody().get("media_id");
 
 //把文件发到群里
 Map<String,Object> params2 = new HashMap<>();
 params2.put("msgtype","file");
 Map<String,String> fileMap = new HashMap<>();
 fileMap.put("media_id",media_id);
 params2.put("file",fileMap);
 Map<String,String> headers2 = new HashMap<>();
 headers.put("Content-Type", "application/json");

 responseObj = TCHttpUtil.sendPostWithObjMap("https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx",headers2,params2);

 //删除html文件
 logger.info("执行删除html文件 ----"+htmlReportPath+ "---" + htmlReport.delete());

经过以上步骤,已经完成消息的发送。需要把其中需要自己替换的更改下,比如机器人链接 key、生成的 json 或 html 路径(如果需要的话)。

八、定时任务,使用 xxljob 负责调度调试就行了。

1、执行器管理-新建执行器

建议选自动注册,当代码持续运行后,会自动写入。当然直接写自己的 ip 也行。【公司一般都有 xxl-job 平台,没有就自己搭建下或者用公司的其他定时任务平台】

2、任务管理-执行器下新建 job 任务

这个 jobHandler 的名字( bugsJob1) 需要和代码中保持一致,先知道下。cron 表达式代表每天的9,14,17,20点执行。也可以自己写,有 cron 在线生成工具。

3、引入依赖

 <dependency>
            <groupId>自己找</groupId>
            <artifactId>xxl-job-core</artifactId>
            <version>arsenal.1.0.1.20200114.1-SNAPSHOT</version>
 </dependency>

4、配置 xxl 信息

加到这个文件里 application.properties

xxl.job.adminAddresses=xxljob域名或ip
xxl.job.appName=qa-bugs - 一定是你写的执行器地址。
xxl.job.accessToken=整个xxljob 就一个accesstoken
xxl.job.logPath=/data/applogs/xxl-job/jobhandler
xxl.job.logRetentionDays

5、xxlconfig 实体类

@Data
@Configuration
@ConfigurationProperties(prefix = XxlJobConfig.XXL_JOB_CONFIG_PREFIX)
public class XxlJobConfig {

    public static final String XXL_JOB_CONFIG_PREFIX = "xxl.job";

    /**
     * 调度中心地址
     */
    private String adminAddresses ;

    /**
     * 对应执行器名称
     */
    private String appName ;

    private String ipe;

    /**
     * 本地连接端口
     */
    private Integer port = 9999;

    /**
     * 访问token
     */
    private String accessToken;

    /**
     * 服务端日志路径
     */
    private String logPath = "/data/applogs/xxl-job/jobhandler";

    /**
     * 日志保留时间
     */
    private Integer logRetentionDays = 60;
    }

6、XXLJob 配置处理类

@Configuration
public class XxlJobExecutorConfig {
    @Resource
    private XxlJobConfig xxlJobConfig;

    @Bean(name = "xxlJobExecutor", initMethod = "start", destroyMethod = "destroy")
    public XxlJobExecutor xxlJobExecutor() {
        XxlJobExecutor xxlJobExecutor = new XxlJobExecutor();
        BeanUtils.copyProperties(xxlJobConfig, xxlJobExecutor);
        return xxlJobExecutor;
    }
}

7、job 执行类

JobHandler 的 value 一定等于你在 xxljob 平台创建的 jobHandler 值,方法的入参s,可以从在 xxljob 平台创建的 job 的入参扔进来,如果你需要用的话。

@JobHandler(value = "bugsJob1")
@Component
public class BugStatisticsJob1 extends IJobHandler {
@Override
    public ReturnT<String> execute(String s) throws Exception {
      
       这个s就是job的入参,有的话就用,没有就忽略。
        logger.info("------推送定时的所有代码(第八步前的所有代码)------");
        return new ReturnT(responseObj.getHttpCode(), (String)null);
        }

代码要写在能持续运行的项目里,比如基于 Spring boot 搭建持续运行的测试平台,或者开发的服务代码里。

其他问题:

1、如果你需要把代码部署到线上,那么先要看看自己线上的数据库是否可以通过数据库地址直连。如果不行,要么去掉数据库校验部分,要么就别部署到线上。你如果是在 qa 写的,就直接部署到 qa 环境的机器上就行了。因为不同环境之前是无法直接访问的,比如用线上的域名访问 qa 的接口。

2、部署到 qa 机器上想正常运行,你就需要去机器上下载之前的所有依赖,本地调通只代表你的电脑本地可以访问,qa 机器上运行就需要保证机器上环境和依赖可用。把 node,apifox-cli,db.json 都准备好。

对于此步骤,我是修改了机器构建部分配置,重点看下这里
dockerfile 和 customCommand 中添加或替换了以下重点几行,自己分辨替换哦。
dockerfile前几行的配置:

FROM docker.17usoft.com/base/tomcat-node:v8   
#FROM docker.17usoft.com/base/tomcat:v8-jdk1.8.0_191
RUN npm install -g apifox-cli  #(下载apifox)
RUN cd /tmp/ && curl -O https://xxxstatic.com/database-connections.json#(我先把db.json上传到文件服务器,进入某个路径并下载)

customCommand前几行的配置

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
#npm install
nvm install v14.17.0 
nvm use v14.17.0 
node -v
npm install --registry=https://nexus.17usoft.com/repository/npm-all/ && npm run build-${serverGroupKey}

3、部署服务器上,基于问题 2,如果 database-connections.json 通过构建参数不好去上传下载,可以直接去对应目录去新建文件,然后把内容复制进入,也可以的。 唯一的不好之处就是需要每次部署后,都要去这样操作。

4、如果机器上安装了 apifox-cli,但是无法访问 apifox 命令(可以进到机器上运行下试试),可以换个思路,尝试使用完整路径去运行。

apifox 命令只是一种快捷访问方式,apifox 命令  等价于     node /usr/local/node-v12.13.0-linux-x64/lib/node_modules/apifox-cli/bin/cli.js  (自己的在哪,自己查下) 替换后也可以运行的。

本来预期这样写

apifox run https://api.apifox.cn/api/v1/api-test/ci-config/359896/detail?token=xlH8c24skOlIwGWN08O-d2 -r json,cli --database-connection ./database-connections.json

可以找到 apifox-cli 的安装地址改成这样写

node /usr/local/node-v12.13.0-linux-x64/lib/node_modules/apifox-cli/bin/cli.js run https://api.apifox.cn/api/v1/api-test/ci-config/359896/detail?token=xlH8c24skOlIwGWN08O-d2 -r json,cli --database-connection ./database-connections.json
订阅
qrcode

订阅

随时随地获取 Apifox 最新动态