HttpRunner 框架
简介
HttpRunner 是一款面向 HTTP(S) 协议的通用测试框架,只需编写维护一份 YAML/JSON 脚本,即可实现自动化测试、性能测试、线上监控、持续集成等多种测试需求 【引用作者简述】
相关链接
框架对比
框架 | 最新版本 | 开发语言 | 支持语言 | 持续集成 | 拓展难度 | 性能测试 | 数据分离 | 推广门槛 | 其它特性 |
---|---|---|---|---|---|---|---|---|---|
Robot Framework | 3.1.2 | python | python/java | 是 | 高 | 不支持 | 支持 | 低 | 自带wx的GUI,可支持界面化或命令操作,可支持web UI自动化seleniumLibrary |
HttpRunner | 2.0 | python | python | 是 | 中 | 支持 | 支持 | 高 | 脚本化、有完善易阅读报告输出 |
Jmeter | 5.1.1 | java | java | 是 | 高 | 支持 | 支持 | 低 | 更偏向于接口性能;做功能测试,用例维护管理难 |
HttpRunner 模块化架构

关于HttpRunner框架详情,在此不做过多介绍,本次内容主要以实战为主
HttpRunner 环境安装
因 python2.7版本已停止更新,不在维护,大部分相关开源项目与库已不再对 python2.x 版本的支持,所以此处用 Python3.6 + HttpRunner 1.5.15 搭建环境
起步:
- pip install -r requirements.txt
requirements.txt
HttpRunner == 1.5.15 Jinja2 == 2.10 PyMySQL == 0.9.3 # 非必需安装,因个人项目中涉及到数据库操作 SQLalchemy == 1.3.4 # 非必需安装,因个人项目中涉及到数据库ORM操作
HttpRunner环境搭建验证
- hrun -v 【使用CLI验证】
- pip list 【通过pip list查看】
CLI 命令 hrun用法
- hrun --startporject projectName 创建工程
- hrun testcase/demo.yml 运行case
- 其它详见 hrun -h
HttpRunner 常用关键字
- name:用例名称
- variables :定义变量
- extract :提取返回结果
- validate: 结果效验
- content 返回结果
- eq 效验
- setup_hooks() 钩子函数,类似于unnitest 的setUp() 执行用例前环境准备
- teardown_hooks() 钩子函数,类似于unnitest的teardown() 用例执行后环境初始化操作
实战
具体以 当前使用的项目为例
用HttpRunner 搭建接口自动化框架概况

工程结构

Api模板注册 Basic.yml
# 登陆 login - api: def: get_token($password, $sign, $timestamp, $userAccount) request: url: /test/login method: POST json: password: $password timestamp: $time_sign userAccount: $userAccount sign: $sign # 用户信息获取 - api: def: get_userInfo($sign, $timestamp, $token) request: url: /test/userInfo method: POST json: sign: $sign timestamp: $time_sign token: $token
用例编写 test.yml
- config: name: 验证用户信息获取接口 request: base_url: $server headers: $m_headers variables: userAccount: '15989556891' password: ${get_pwd(123456)} childIds: '11027897,11029010' validate: - eq: [status_code, 200] - test: name: 获取 token api: get_token($password,$sign,$timestamp,$userAccount) - test: name: case 01 验证token错误时,返回是否正确 variables: token: 'sfsd12' api: get_userInfo($sign,$timestamp,$token) extract: - code: content.code - msg: content.errorMsg validate: - eq: ['$code', '00600010006'] - eq: ['$msg', '无效的token'] - test: name: case 02 验证token过期,返回是否正确 variables: token: 616383b06cbf8ce4d392ff4523670058000050900 api: get_userInfo($sign,$timestamp,$token) extract: - code: content.code - msg: content.errorMsg validate: - eq: ['$code', '00200010006'] - eq: ['$msg', 'token已过有效期'] - test: name: case 03 token有效,正常获取用户信息,返回是否正确 api: get_userInfo($sign,$timestamp,$token) extract: - code: content.code - data: content.data validate: - eq: ['$code', '000'] - eq: ['$data', '返回数据内容']
HttpRunner 结果报告 Report.html

报告拓展 -> 邮件报告

报告拓展 -> 钉钉机器人提醒

拓展 源码:邮件 + 钉钉
#!/usr/bin/env python # -*- coding: UTF-8 -*- # Created by Hank on 2019-07-15. import time import json from setting import SERVER_ID class HtmlTemplate: """ Email content HtmlTemplate Attributes: """ def __init__(self, summary): self.summary = summary def _get_summary(self): """ get all case run detail :return: """ detail = self.summary['details'] time_lst, name_lst, case_detail = [], [], [] if detail: for i in range(len(detail)): for j in range(len(detail[i]['records'])): case_name = detail[i]['records'][j]['name'] case_status = detail[i]['records'][j]['status'] case_api = detail[i]['records'][j]['meta_data']['request']['url'] if case_api != 'N/A': case_code = detail[i]['records'][j]['meta_data']['response']['status_code'] res_time = detail[i]['records'][j]['meta_data']['response']['response_time_ms'] if case_code != 200: res_text = json.dumps({'code': 'HTTP ' + str(case_code)}) else: res_text = detail[i]['records'][j]['meta_data']['response']['text'] case_response = [case_status, case_name, case_code, case_api, res_text] time_lst.append(res_time) name_lst.append(case_name) case_detail.append(case_response) return { 'detail': detail, 'time_lst': time_lst, 'name_lst': name_lst, 'case_detail': case_detail } def _get_time_analy(self): rsp_data = self._get_summary() time_lst = rsp_data['time_lst'] name_lst = rsp_data['name_lst'] aly_time, aly_name = [], [] if time_lst: for i in range(len(time_lst)): if int(time_lst[i]) >= 2000: aly_time.append(time_lst[i]) aly_name.append(name_lst[i]) return { 'aly_time': aly_time, 'aly_name': aly_name } def _get_case_analy(self): rsp_data = self._get_summary()['case_detail'] rlt = [] if rsp_data: for i in range(len(rsp_data)): if rsp_data[i][0] != 'success': rsp_body = json.loads(rsp_data[i][4]) rsp_data[i][3] = rsp_data[i][3][len(SERVER_ID):] if rsp_body['code'] == '001': rsp_data[i][4] = 'code:001,data与预期不一致' rlt.append(rsp_data[i]) return rlt def __table_total(self): """ case执行数统计 + 请求耗时统计 模块 :return: 返回 html模板 type(str) """ rsp_data = self._get_summary() time_lst = rsp_data['time_lst'] name_lst = rsp_data['name_lst'] duration = round(self.summary['time']['duration'], 3) sum = 0 for k in range(len(time_lst)): sum += time_lst[k] avg_time = round(sum / len(time_lst), 3) # 耗时最多的 api 详情 max_index = time_lst.index(max(time_lst)) max_case_name = name_lst[max_index] # Server path server_path = SERVER_ID[len('https://'):] case_result = """ <h4>Api check Report</h4> <th>""" + '详见附件...' + """</th> <br/> <td>""" + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + """</td> <table width="800" border="1" bordercolor="#FFFFFF" style="border-collapse:collapse;" cellspacing="1" cellpadding="4" bgcolor="#66CCFF" class="tabtop13" font-size="16px" padding-left="15px"> <tr> <td colspan="5" align="center" font-size="24"><strong>""" + 'API CheckReport' + """</strong></td> </tr> <tr> <td>""" + 'START AT' + """</td> <td colspan="4" align="center">""" + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + """</td> </tr> <tr> <td>""" + 'DURATION' + """</td> <td colspan="4" align="center">""" + str(duration) + 'seconds' + """</td> </tr> <tr> <td>""" + 'SERVER' + """</td> <td colspan="4" align="center">""" + server_path + """</td> </tr> <tr> <td width="160">""" + 'TOTAL' + """</td> <td width="160">""" + 'SUCCESS' + """</td> <td width="160">""" + 'FAILED' + """</td> <td width="160">""" + 'ERROR' + """</td> <td width="160">""" + 'SKIPPED' + """</td> </tr> <tr> <td bgcolor="#C8EDFF">""" + str(self.summary['stat']['testsRun']) + """</td> <td bgcolor="#C8EDFF">""" + str(self.summary['stat']['successes']) + """</td> <td bgcolor="#C8EDFF">""" + str(self.summary['stat']['failures']) + """</td> <td bgcolor="#C8EDFF">""" + str(self.summary['stat']['errors']) + """</td> <td bgcolor="#C8EDFF">""" + str(self.summary['stat']['skipped']) + """</td> </tr> </table> <p> </p> <table width="800" border="1" bordercolor="#FFFFFF" style="border-collapse:collapse;" cellspacing="1" cellpadding="4" bgcolor="#66CCFF" class="tabtop13" font-size="16px" padding-left="15px"> <tr> <td colspan="5" align="center" font-size="24"><strong>""" + 'Responses Time' + """</strong></td> </tr> <tr> <td width="266">""" + 'MAX' + """</td> <td width="266">""" + 'MIN' + """</td> <td width="266">""" + 'AVG' + """</td> </tr> <tr> <td bgcolor="#C8EDFF">""" + str(max(time_lst)) + 'ms' + """</td> <td bgcolor="#C8EDFF">""" + str(min(time_lst)) + 'ms' + """</td> <td bgcolor="#C8EDFF">""" + str(avg_time) + 'ms' + """</td> </tr> <tr> <td colspan="3" bgcolor="#C8EDFF">""" + '耗时最大api:' + str(max_case_name) + """</td> </tr> </table> """ return case_result def __table_time(self): """ 接口响应时长分析 模块-content :return: """ table_td = '' aly_time = self._get_time_analy()['aly_time'] aly_name = self._get_time_analy()['aly_name'] if len(aly_time) == len(aly_name): for i in range(len(aly_time)): test_tmp = """ <table width="800" border="1" bordercolor="#FFFFFF" style="border-collapse:collapse;" cellspacing="1" cellpadding="4" bgcolor="#fa8072" class="tabtop13" font-size="16px" padding-left="15px"> <tr> <td width="80">""" + str(aly_time[i]) + 'ms' + """</td> <td width="840">""" + str(aly_name[i]) + """</td> </tr> </table> """ table_td += str(test_tmp) return table_td def __table_time_module(self): """ 错误信息统计和分析 模块 :return: """ table_header = """ <p> </p> <table width="800" border="1" bordercolor="#FFFFFF" style="border-collapse:collapse;" cellspacing="1" cellpadding="4" bgcolor="#66CCFF" class="tabtop13" font-size="16px" padding-left="15px"> <tr> <td colspan="5" align="center" font-size="24"><strong>""" + 'Slow Responses' + """</strong></td> </tr> <tr> <td width="80">""" + 'TIME' + """</td> <td width="720">""" + 'API' + """</td> </tr> </table> """ return table_header + self.__table_time() def __table_assembly(self): rsp_data = self._get_case_analy() table_td = '' if rsp_data: for i in range(len(rsp_data)): test_tmp = """ <table width="920" border="1" bordercolor="#FFFFFF" style="border-collapse:collapse;" cellspacing="1" cellpadding="4" bgcolor="#fa8072" class="tabtop13" font-size="16px" padding-left="15px"> <tr> <td width="80">""" + str(rsp_data[i][0]) + """</td> <td width="80">""" + str(rsp_data[i][2]) + """</td> <td width="360">""" + str(rsp_data[i][1]) + """</td> <td width="360">""" + str(rsp_data[i][4]) + """</td> <td width="320">""" + str(rsp_data[i][3]) + """</td> </tr> </table> """ table_td += str(test_tmp) return table_td def __table_error_module(self): """ 错误信息统计和分析 模块 :return: """ table_header = """ <p> </p> <table width="920" border="1" bordercolor="#FFFFFF" style="border-collapse:collapse;" cellspacing="1" cellpadding="4" bgcolor="#66CCFF" class="tabtop13" font-size="16px" padding-left="15px"> <tr> <td colspan="5" align="center" font-size="24"><strong>""" + 'Error Analysis' + """</strong></td> </tr> <tr> <td width="80">""" + 'STATUS' + """</td> <td width="80">""" + 'CODE' + """</td> <td width="320">""" + 'CASE_NAME' + """</td> <td width="360">""" + 'CASE_DETAIL' + """</td> <td width="360">""" + 'API' + """</td> </tr> </table> """ return table_header + self.__table_assembly() def dd_analy(self): rsp = self._get_summary() time_lst = rsp['time_lst'] case_detail = rsp['case_detail'] aly_time, aly_case = [], [] if time_lst: for i in range(len(time_lst)): if int(time_lst[i]) >= 2000: aly_time.append([time_lst[i], case_detail[i][3]]) if case_detail: for j in range(len(case_detail)): rsp_code = json.loads(case_detail[j][4]) rsp_status = case_detail[j][0] if case_detail[j][2] == 200: if rsp_code['code'] != '00000' and rsp_status != 'success': aly_case.append([case_detail[j][3], rsp_code['errorMsg']]) else: aly_case.append([case_detail[j][3], case_detail[j][2]]) return { 'aly_time': aly_time, 'aly_case': aly_case, } def html_temp(self): """ 数据统计 模板 html for Email :return: html 模板 """ speed_lst = self._get_time_analy()['aly_time'] analy_lst = self._get_case_analy() rp = '' if len(speed_lst) >= 1 and len(analy_lst) >= 1: rp = self.__table_total() + self.__table_time_module() + self.__table_error_module() elif len(speed_lst) >= 1 or len(analy_lst) >= 1: if len(speed_lst) >= 1: rp = self.__table_total() + self.__table_time_module() else: rp = self.__table_total() + self.__table_error_module() return rp if __name__ == '__main__': pass
来源:https://blog.csdn.net/baidu_27032161/article/details/100537375