前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >测试结果存储并使用图表展示

测试结果存储并使用图表展示

作者头像
zx钟
发布2022-01-18 15:57:04
7670
发布2022-01-18 15:57:04
举报
文章被收录于专栏:测试游记测试游记

流程

每次执行完测试之后将测试结果插入数据库

使用Spring Boot+MyBatis读取数据

前端通过接口获取处理后的数据并在图表上展示

最终展示

数据表创建

数据源来自于pytest执行之后的结果,由于使用allure进行结果的保存,所以直接读取对应的测试结果文件

  1. 解析报告存储路径,拿到包含'-result.json'名称的文件
  2. 遍历json文件,读取到测试结果信息
  3. 处理重复执行数据

json文件

代码语言:javascript
复制
{
  "name": "处方购药",
  "status": "skipped",
  "statusDetails": {
    "message": "Skipped: 非测试环境,不执行该用例",
    "trace": "('/Users/zhongxin/gitproject/wytest/src/cases_ui/2021Q4/test_whole_process.py', 133, 'Skipped: 非测试环境,不执行该用例')"
  },
  "start": 1640090078320,
  "stop": 1640090078320,
  "uuid": "e1333fd7-ed19-47a3-8cb8-6bfe532d0e5b",
  "historyId": "2489564d3e1374d96c407b6c6352448f",
  "testCaseId": "32b23ec92bffbdf659133aeb8d49dc00",
  "fullName": "src.cases_ui.2021Q4.test_whole_process#test_buy_medicine",
  "labels": [
    {
      "name": "story",
      "value": "全流程测试-测试环境"
    },
    {
      "name": "feature",
      "value": "微医主站"
    },
    {
      "name": "tag",
      "value": "dependency(name='buy_medicine', depends=['prescription'])"
    },
    {
      "name": "tag",
      "value": "h5"
    },
    {
      "name": "tag",
      "value": "@pytest.mark.skipif(True, reason='非测试环境,不执行该用例')"
    },
    {
      "name": "parentSuite",
      "value": "src.cases_ui.2021Q4"
    },
    {
      "name": "suite",
      "value": "test_whole_process"
    },
    {
      "name": "host",
      "value": "localhost"
    },
    {
      "name": "thread",
      "value": "1548-MainThread"
    },
    {
      "name": "framework",
      "value": "pytest"
    },
    {
      "name": "language",
      "value": "cpython3"
    },
    {
      "name": "package",
      "value": "src.cases_ui.2021Q4.test_whole_process"
    }
  ]
}

代码

代码语言:javascript
复制
def get_allure_result(self, path=f'{REPORT_PATH}/allure_results'):
    """
    读取allure测试结果
    :param path: 存放allure运行json结果的文件夹
    :return:
    """
    result_list = [i for i in os.listdir(path) if '-result.json' in i]
    allure_results = []
    allure_dict = {}
    for i in result_list:
        json_data = self.read_json(os.path.join(path, i))
        full_name = json_data.get("fullName")
        labels = '_'.join([i.get('value', '') for i in json_data.get("labels") if
                           i.get('name', '') in ['feature', 'story']]) + "_" + json_data.get("name")
        parameters = json_data.get("parameters", '') and '_'.join([i.get('value', '') for i in
                                                                   json_data.get("parameters", '')])
        statusDetails = json_data.get('statusDetails', '')
        statusDetails1 = {
            "message": "",
            "trace": ""
        }
        if statusDetails:
            try:
                statusDetails1["message"] = statusDetails.get("message", "")
                statusDetails1["trace"] = statusDetails.get("trace", "")
            except Exception:
                pass
        data = {
            "fullName": full_name,
            "status": json_data.get("status"),
            "labels": labels,
            "duration": json_data.get("stop", 0) - json_data.get("start", 0),
            "parameters": parameters,
            "statusDetails": statusDetails1
        }
        if f'{full_name}_{parameters}' not in allure_dict:
            allure_dict[f'{full_name}_{parameters}'] = [data]
        else:
            allure_dict[f'{full_name}_{parameters}'].append(data)
    for v in allure_dict.values():
        if len(v) == 1:
            allure_results.append(v[0])
        else:
            for v1 in v:
                if 'pass' in v1.get("status"):
                    allure_results.append(v1)
                    break
            else:
                allure_results.append(v[0])
    return allure_results

数据表

我们将需要的信息存放到数据库中,所以设计的数据表如下:

代码语言:javascript
复制
create table ui_report (
    id           int(11) unsigned auto_increment comment 'ID'
        primary key,
    project_name varchar(255)  null comment 'Jenkins Job名称',
    build_num    varchar(255)  null comment '构建编号',
    code_name    varchar(255)  null comment '用例名称',
    status       varchar(50)   null comment '运行结果',
    case_desc    varchar(255)  null comment '用例描述',
    duration     int default 0 null comment '执行耗时',
    parameters   varchar(255)  null comment '用例参数',
    message      varchar(255)  null comment '错误信息',
    gmt_created  datetime      not null comment '创建时间',
    gmt_modified datetime      not null comment '修改时间'
)
    comment 'UI用例执行结果';

数据插入

ORM

使用peewee模块进行数据库ORM操作

将数据库映射成python代码

代码语言:javascript
复制
from peewee import *

database = MySQLDatabase('auto_test', **{'charset': 'utf8', 'sql_mode': 'PIPES_AS_CONCAT', 'use_unicode': True,'host': '', 'port': 3306,'user': '', 'password': ''})


class UnknownField(object):
    def __init__(self, *_, **__): pass


class BaseModel(Model):
    class Meta:
        database = database


class UiReport(BaseModel):
    project_name = CharField(null=True)
    build_num = CharField(null=True)
    case_desc = CharField(null=True)
    code_name = CharField(null=True)
    duration = IntegerField(constraints=[SQL("DEFAULT 0")], null=True)
    parameters = CharField(null=True)
    status = CharField(null=True)
    message = CharField(null=True)
    gmt_created = DateTimeField()
    gmt_modified = DateTimeField()

    class Meta:
        table_name = 'ui_report'

插入数据

在执行完测试之后进行插入数据操作

conftest.py文件的pytest_terminal_summary钩子函数中处理数据插入

代码语言:javascript
复制
def pytest_terminal_summary(terminalreporter, exitstatus, config):
    """
    统计测试结果
    :param terminalreporter:
    :param exitstatus:
    :param config:
    :return:
    """
    logger.info(f"总计:{terminalreporter._numcollected}")
    stats = terminalreporter.stats
    failed_num = len(stats.get('failed', []))
    error_num = len(stats.get('error', []))
    logger.info(f"通过:{len(stats.get('passed', []))}")
    logger.info(f"失败:{failed_num}")
    logger.info(f"异常:{error_num}")
    logger.info(f"跳过:{len(stats.get('skipped', []))}")
    duration = time.time() - terminalreporter._sessionstarttime
    logger.info(f"总耗时:{duration}秒")
    if not hasattr(config, "workerinput"):
        jsonoperator = JsonOperator()
        allure_results = jsonoperator.get_allure_result()
        if ROBOT:
            try:
                r = ReportOperator(hook=ROBOT.split(','))
                r.save_result_magic(allure_results)
            except Exception as e:
                logger.error(f'存入数据库失败:{e}')

save_result_magic遍历刚才读取json拿到的内容然后依次插入数据

代码语言:javascript
复制
def save_result_magic(self, run_detail):
    """
    将运行结果存储到magic数据库
    """
    logger.info(run_detail)
    if ProjectName != "非jenkins运行":
        for i in run_detail:
            try:
                UiReport(
                    project_name=ProjectName,
                    build_num=f"{timeoperator.now2}_{BUILD_NUMBER}",
                    case_desc=i.get("labels"),
                    code_name=i.get("fullName"),
                    duration=i.get("duration"),
                    parameters=i.get("parameters"),
                    status=i.get("status"),
                    message=i["statusDetails"].get("message", "")[:255],
                    gmt_created=timeoperator.now1,
                    gmt_modified=timeoperator.now1
                ).save()
            except Exception as e:
                logger.error(f"存储数据{i}失败:「{e}」")

Spring Boot数据处理

目的

通过用例执行明显拿到每天的执行情况

Do对象

我们在图表中要展示的数据如下

代码语言:javascript
复制
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
public class UiReportDayDo {
    /**
     * Jenkins Job名称
     */
    String projectName;
    /**
     * 执行时间,格式:年-月-日
     */
    String time;
    /**
     * 运行总次数
     */
    Long runTimes;
    /**
     * 失败总次数
     */
    Long errorNum;
}

但是我们存入的数据和这个数据差别比较大,所以我们需要使用SQL进行一轮处理

SQL处理

根据 Job名称、执行编号进行分组 拿到执行状态是failedbroken的数据

代码语言:javascript
复制
select 
    ui.project_name,
    ui.build_num,
    sum(IF(ui.status = 'failed' || ui.status = 'broken', 1, 0)) as error_num
from ui_report as ui
group by ui.build_num, ui.project_name

然后

  • build_num进行切割取前面10个字符
  • 如果error_num等于0则为0,不然显示1,这样就拿到了每个build_num是否有错误
代码语言:javascript
复制
select a.project_name, left(a.build_num, 10) as time, IF(a.error_num = 0, 0, 1) as error_num
from (
    select ui.project_name, ui.build_num,
        sum(IF(ui.status = 'failed' || ui.status = 'broken', 1, 0)) as error_num
    from ui_report as ui
    group by ui.build_num, ui.project_name
) as a

最后

  • count(*) as run_times 每天的总执行次数
  • sum(b.error_num) as error_num 每天的总失败次数
代码语言:javascript
复制
select b.project_name as project_name, b.time, count(*) as run_times, sum(b.error_num) as error_num
from (
    select a.project_name, left(a.build_num, 10) as time, IF(a.error_num = 0, 0, 1) as error_num
    from (
        select ui.project_name, ui.build_num,
            sum(IF(ui.status = 'failed' || ui.status = 'broken', 1, 0)) as error_num
        from ui_report as ui
        group by ui.build_num, ui.project_name
    ) as a
) as b

编写Mapper

代码语言:javascript
复制
public interface UiReportMapper {

    /**
     * 分页获取每日执行情况
     *
     * @return
     */
    @PageMapper(listStatement = "findDayByQuery", countStatement = "countDayByQuery")
    Page<UiReportDayDo> pageDayQuery(UiReportPageQuery uiReportPageQuery);
}

编写xml

需要进行分页+查询所以xml需要有

  • findDayByQuery
  • countDayByQuery
代码语言:javascript
复制
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.guahao.qa.magicapiweb.biz.dal.uireport.mapper.UiReportMapper">
    <sql id="allFields">
        id
        ,project_name,build_num,code_name,status,case_desc,duration,parameters,message,gmt_created,gmt_modified
    </sql>

    <sql id="queryDayInfo">
        select b.project_name as project_name, b.time, count(*) as run_times, sum(b.error_num) as error_num
        from (
            select a.project_name, left(a.build_num, 10) as time, IF(a.error_num = 0, 0, 1) as error_num
            from (
                select ui.project_name, ui.build_num,
                sum(IF(ui.status = 'failed' || ui.status = 'broken', 1, 0)) as error_num
                from ui_report as ui
                group by ui.build_num, ui.project_name
                ) as a
        ) as b
    </sql>

    <sql id="queryDayCondition">
        <where>
            <if test="projectName!= null and projectName!=''">and b.project_name= #{projectName}</if>
            <if test="buildNum!= null and buildNum!=''">and b.time like CONCAT('%', #{buildNum}, '%')</if>
        </where>
    </sql>


    <select id="findDayByQuery" resultType="com.guahao.qa.magicapiweb.biz.dal.uireport.model.UiReportDayDo">
        <include refid="queryDayInfo"/>
        <include refid="queryDayCondition"/>
        group by b.time,b.project_name
        order by b.time desc
        LIMIT #{offset}, #{pageSize}
    </select>

    <select id="countDayByQuery" resultType="long">
        SELECT count(*)
        FROM (
        <include refid="queryDayInfo"/>
        <include refid="queryDayCondition"/>
        group by b.time,b.project_name
        order by b.time desc
        ) as c
    </select>

</mapper>

编写Manager

代码语言:javascript
复制
public interface UiReportManager {
    /**
     * 获取每日执行情况
     *
     * @return
     */
    Page<UiReportDayDo> getCalcDay(@NotNull UiReportPageQuery uiReportPageQuery);
}

编写Impl

代码语言:javascript
复制
@Service
@Slf4j
@Validated
public class UiReportManagerImpl implements UiReportManager {
    @Override
    public Page<UiReportDayDo> getCalcDay(UiReportPageQuery pageQuery) {
        return uiReportMapper.pageDayQuery(pageQuery);
    }

}

编写Controller

代码语言:javascript
复制
@RestController
@Slf4j
@CrossOrigin(origins = "*")
@RequestMapping("/uireport")
public class UiReportController {

    /**
     * 获取每日运行情况
     *
     * @return
     */
    @RequestMapping(value = "/queryUiDayList", method = RequestMethod.POST, produces = {
        "application/json;charset=UTF-8" })
    public Result<Page> queryUiDayList(@RequestBody UiReportPageQuery uiReportPageQuery) {
        Page<UiReportDayDo> calcCase = uiReportManager.getCalcDay(uiReportPageQuery);
        return Results.success(calcCase);
    }
}

前端请求

前端请求/uireport/queryUiDayList就可以拿到执行数据

代码语言:javascript
复制
{"code":"0","data":{"pageNumber":1,"pageSize":10,"results":[{"errorNum":7,"projectName":"pipeline_h5_Python","runTimes":20,"time":"2022-01-02"},{"errorNum":20,"projectName":"pipeline_h5_Python","runTimes":24,"time":"2022-01-01"},{"errorNum":10,"projectName":"pipeline_h5_Python","runTimes":24,"time":"2021-12-31"},{"errorNum":10,"projectName":"pipeline_h5_Python","runTimes":24,"time":"2021-12-30"},{"errorNum":5,"projectName":"pipeline_h5_Python","runTimes":21,"time":"2021-12-29"},{"errorNum":5,"projectName":"pipeline_h5_Python","runTimes":9,"time":"2021-12-28"}],"totalCount":6,"totalPages":1},"message":"成功!"}

每次获取数据后都去重新渲染一次图表

代码语言:javascript
复制
async getDayInfo() {
  const response = await UiReport.apiGetDayInfo(this.searchData)
  if (response.status === 200) {
    this.calcData = { errorNum: [], successNum: [], rate: [] }
    this.time_list = []
    this.dayCaseInfo = response.data.data.results
    this.totalCount = response.data.data.totalCount
    this.dayCaseInfo.forEach(item => {
      const rate = ((item.runTimes - item.errorNum) / item.runTimes).toFixed(3) * 100
      item.rate = rate + '%'
      this.time_list.push(item.time)
      this.calcData.errorNum.push(item.errorNum)
      this.calcData.successNum.push(item.runTimes - item.errorNum)
      this.calcData.rate.push(rate + '')
    })
    this.$nextTick(() => {
      this.showCharts()
    })
  }
},

查询某个项目

查询某个项目的某个时间

然后使用vue进行展示

代码语言:javascript
复制
<template>
  <div>
    <el-form :data="searchData" :inline="true">
      <el-form-item>
        <el-select v-model="searchData.projectName" placeholder="请选择Job" clearable filterable>
          <el-option v-for="(item, index) in $store.state.ui.jobList" :key="index" :label="item" :value="item"></el-option>
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-input v-model="searchData.buildNum" placeholder="请输入日期" clearable></el-input>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="getDayInfo">查询</el-button>
      </el-form-item>
    </el-form>
    <div class="tableDiv">
      <div id="CalcReportChart" style="width:1200px;height:500px"></div>
    </div>
    <el-table :data="dayCaseInfo" :header-cell-style="{ background: '#0e2642', color: '#fff' }" border>
      <el-table-column prop="projectName" label="Job名称"></el-table-column>
      <el-table-column prop="time" label="日期"></el-table-column>
      <el-table-column prop="runTimes" label="执行次数"></el-table-column>
      <el-table-column prop="errorNum" label="错误次数"></el-table-column>
      <el-table-column prop="rate" label="成功率"></el-table-column>
    </el-table>
    <el-pagination
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
      :current-page="searchData.pageNumber"
      :page-sizes="[10, 20, 50, 100]"
      layout="total, sizes, prev, pager, next, jumper"
      :total="totalCount"
    ></el-pagination>
  </div>
</template>

<script>
import UiReport from '@/api/ui/index.js'
import * as echarts from 'echarts'
export default {
  components: {},
  props: {},
  data() {
    return {
      dayCaseInfo: [],
      searchData: {
        pageNumber: 1,
        pageSize: 10,
        projectName: 'pipeline_h5_Python',
        buildNum: ''
      },
      totalCount: 0,
      calcData: { errorNum: [], successNum: [], rate: [] },
      time_list: []
    }
  },
  computed: {},
  watch: {},
  methods: {
    // 获取每天运行结果
    async getDayInfo() {
      const response = await UiReport.apiGetDayInfo(this.searchData)
      if (response.status === 200) {
        this.calcData = { errorNum: [], successNum: [], rate: [] }
        this.time_list = []
        this.dayCaseInfo = response.data.data.results
        this.totalCount = response.data.data.totalCount
        this.dayCaseInfo.forEach(item => {
          const rate = ((item.runTimes - item.errorNum) / item.runTimes).toFixed(3) * 100
          item.rate = rate + '%'
          this.time_list.push(item.time)
          this.calcData.errorNum.push(item.errorNum)
          this.calcData.successNum.push(item.runTimes - item.errorNum)
          this.calcData.rate.push(rate + '')
        })
        this.$nextTick(() => {
          this.showCharts()
        })
      }
    },
    handleSizeChange(val) {
      this.searchData.pageNumber = 1
      this.searchData.pageSize = val
      this.getDayInfo()
    },
    handleCurrentChange(val) {
      this.searchData.pageNumber = val
      this.getDayInfo()
    },
    createCalcData() {
      const _this = this
      var xData = _this.time_list
      const option = {
        backgroundColor: '#0e2642',
        title: {
          show: true,
          text: _this.searchData.projectName,
          textStyle: {
            color: '#fff'
          },
          x: 'center'
        },
        legend: {
          data: ['成功次数', '错误次数', '成功率'],
          left: '18%',
          top: 30,
          itemWidth: 16.7,
          itemHeight: 7.6,
          type: 'plain',
          textStyle: {
            color: 'RGBA(154, 209, 253, 1)'
          }
        },
        tooltip: {
          trigger: 'axis',
          axisPointer: {
            type: 'shadow',
            textStyle: {
              color: '#fff'
            }
          }
        },
        grid: {
          borderWidth: 0,
          top: 100,
          bottom: 80,
          left: 55,
          right: 45,
          textStyle: {
            color: '#fff'
          }
        },
        calculable: true,
        xAxis: [
          {
            type: 'category',
            axisLine: {
              lineStyle: {
                color: 'rgba(255,255,255,.5)'
              }
            },
            splitLine: {
              show: false
            },
            axisTick: {
              show: false
            },
            splitArea: {
              show: false
            },
            axisLabel: {
              interval: 0,
              color: 'rgba(255,255,255,0.7)',
              fontSize: 10
            },
            data: xData
          }
        ],
        yAxis: [
          {
            name: '总次数',
            nameTextStyle: {
              color: '#FDFDFD',
              padding: [0, 0, 0, -50]
            },
            nameGap: 15,
            type: 'value',
            splitLine: {
              show: true,
              lineStyle: {
                type: 'dashed',
                color: 'RGBA(3, 75, 97, 1)'
              }
            },
            axisLine: {
              show: false
            },
            axisTick: {
              show: false
            },
            axisLabel: {
              interval: 0,
              color: 'rgba(255,255,255,0.5)',
              fontSize: 10
            },
            splitArea: {
              show: false
            }
          },
          {
            name: '成功率',
            nameTextStyle: {
              color: '#FDFDFD',
              padding: [0, 0, 0, 35]
            },
            type: 'value',
            splitLine: {
              show: false
            },
            min: 0,
            axisLabel: {
              interval: 0,
              color: 'rgba(255,255,255,0.5)',
              fontSize: 10,
              formatter: '{value}%'
            }
          }
        ],
        series: [
          {
            name: '成功次数',
            type: 'bar',
            stack: '1',
            barMaxWidth: 15,
            barGap: '10%',
            itemStyle: {
              normal: {
                color: {
                  type: 'linear',
                  x: 0,
                  y: 0,
                  x2: 0,
                  y2: 1,
                  colorStops: [
                    {
                      offset: 0,
                      color: '#2764CA' // 0% 处的颜色
                    },
                    {
                      offset: 1,
                      color: '#6D9EEE' // 100% 处的颜色
                    }
                  ],
                  global: false // 缺省为 false
                },
                opacity: 1
              }
            },
            data: _this.calcData.successNum
          },
          {
            name: '错误次数',
            type: 'bar',
            stack: '1',
            itemStyle: {
              normal: {
                color: {
                  type: 'linear',
                  x: 0,
                  y: 0,
                  x2: 0,
                  y2: 1,
                  colorStops: [
                    {
                      offset: 0,
                      color: '#FF8B77' // 0% 处的颜色
                    },
                    {
                      offset: 1,
                      color: '#FE6AAC' // 100% 处的颜色
                    }
                  ],
                  global: false // 缺省为 false
                },
                opacity: 0.9,
                barBorderRadius: 0
              }
            },
            data: _this.calcData.errorNum
          },
          {
            name: '成功率',
            type: 'line',
            yAxisIndex: 1,
            symbolSize: 0,
            symbol: 'emptyCircle',
            itemStyle: {
              normal: {
                color: '#FFC130',
                barBorderRadius: '100%'
              },
              borderWidth: 0
            },
            smooth: true,
            lineStyle: {
              normal: {
                width: 3,
                color: {
                  type: 'linear',
                  x: 0,
                  y: 0,
                  x2: 0,
                  y2: 1,
                  colorStops: [
                    {
                      offset: 0,
                      color: '#FF8B77' // 0% 处的颜色
                    },
                    {
                      offset: 1,
                      color: '#FE6AAC' // 100% 处的颜色
                    }
                  ],
                  global: false // 缺省为 false
                }
              }
            },
            data: _this.calcData.rate
          }
        ]
      }
      return option
    },
    showCharts() {
      var myChart = echarts.init(document.getElementById('CalcReportChart'))
      const options = this.createCalcData()
      console.log(options)
      myChart.setOption(options)
    }
  },
  mounted() {
    this.showCharts()
  },
  created() {
    this.getDayInfo()
  }
}
</script>
<style scoped></style>
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-01-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 测试游记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 流程
  • 数据表创建
    • json文件
      • 代码
        • 数据表
        • 数据插入
          • ORM
            • 插入数据
            • Spring Boot数据处理
              • 目的
                • Do对象
                  • SQL处理
                    • 编写Mapper
                      • 编写xml
                        • 编写Manager
                          • 编写Impl
                            • 编写Controller
                            • 前端请求
                            领券
                            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档