自动化工具 探索 HttpRunner 最佳体现形式_设想篇

尹全旺 · September 07, 2018 · Last by fengyu replied at February 20, 2019 · 2390 hits

背景

之前已经在社区开源过HttpRunnerManager,由于之前设计过于简单,导致后面功能扩展带来了很大局限性,HttpRunner很多特性也不能很好的完全实现,本人是@debugtalk的忠实粉丝,一直都在探索HttpRunner最佳体现形式,结合自己使用HttpRunner的实践和痛点,这次给大家带来了一款小而美的测试工具,目前雏形已经完成,计划后续开源在社区,提前发表出来也是希望使用HttpRunner的童鞋们多多给提提需求

预期目标

  1. 方便后续自行扩展,根据自己需求进行改造
  2. 支持CLI命令行启动,通过参数指定文件夹可以把HttpRunner脚本直接web化
  3. 支持平台化部署,可以导入导出标准yaml或json格式脚本
  4. 小白使用应该也能快速上手

技术选型

本次采用前后端分离方式,后端还是采用了自己最熟悉的django开发,结合django_restful提供restful接口, 前端采用了目前比较热的vue.js + element-ui

设计思路

  1. 为了迎合对于HttpRunner版本快速迭代和易于扩展要求,首先在前端工程结构设计方面我们可以分为pages, router, store, restful assets这几层
    • FasterRunnerWeb
      • assets
      • router
      • pages
      • store
      • restful

其中restful管理所有前后端交互的接口,pages里对于HttpRunner的所有关键字都采用组件形式进行模块化封装,如下所示:

后端还是采用了MVT模式,没有特意进行文件夹划分,所有数据表放置在models.py 所有类视图在views.py, 其他操作都置于utils包下

数据交互

简单的增删改查就不说了,关于HttpRunner数据结构和前端数据结构是重点,一开始我们必须约定好双方格式而且实现两个解析器

  1. 后端数据结构及解析类:

    class Format(object):
    """
    解析标准HttpRunner脚本 前端->后端
    """


    def __init__(self, body):
    """
    body => {
    header: header -> [{key:'', value:'', desc:''},],
    request: request -> {
    form: formData - > [{key: '', value: '', type: 1, desc: ''},],
    json: jsonData -> {},
    params: paramsData -> [{key: '', value: '', type: 1, desc: ''},]
    files: files -> {"
    fields","binary"}
    },
    extract: extract -> [{key:'', value:'', desc:''}],
    validate: validate -> [{expect: '', actual: '', comparator: 'equals', type: 1},],
    variables: variables -> [{key: '', value: '', type: 1, desc: ''},],
    hooks: hooks -> [{setup: '', teardown: ''},],
    url: url -> string
    method: method -> string
    name: name -> string
    }
    """


    try:
    self.name = body.pop('name')
    self.url = body.pop('url')
    self.method = body.pop('method')
    self.times = body.pop('times')

    self.headers = body['header'].pop('header')
    self.params = body['request']['params'].pop('params')
    self.data = body['request']['form'].pop('data')
    self.json = body['request'].pop('json')
    self.files = body['request']['files'].pop('files')

    self.variables = body['variables'].pop('variables')
    self.extract = body['extract'].pop('extract')
    self.validate = body.pop('validate').pop('validate')
    self.setup_hooks = body['hooks'].pop('setup_hooks')
    self.teardown_hooks = body['hooks'].pop('teardown_hooks')

    self.desc = {
    "header": body['header'].pop('desc'),
    "data": body['request']['form'].pop('desc'),
    "files": body['request']['files'].pop('desc'),
    "params": body['request']['params'].pop('desc'),
    "extract": body['extract'].pop('desc'),
    "variables": body['variables'].pop('desc'),
    }

    self.relation = body.pop('nodeId')
    self.project = body.pop('project')

    self.testcase = None
    except KeyError:
    pass

    def parse_test(self):
    """
    返回标准化HttpRunner "
    desc" 字段运行需去除
    """

    test = {
    "name": self.name,
    "times": self.times,
    "request": {
    "url": self.url,
    "method": self.method
    },
    "desc": self.desc
    }

    if self.headers:
    test["request"]["headers"] = self.headers
    if self.params:
    test["request"]["params"] = self.params
    if self.data:
    test["request"]["data"] = self.data
    if self.json:
    test["request"]["json"] = self.json
    if self.files:
    test["request"]["files"] = self.files

    if self.extract:
    test["extract"] = self.extract
    if self.validate:
    test['validate'] = self.validate
    if self.variables:
    test["variables"] = self.variables
    if self.setup_hooks:
    test['setup_hooks'] = self.setup_hooks
    if self.teardown_hooks:
    test['teardown_hooks'] = self.teardown_hooks

    self.testcase = test
  2. 前端数据解析类:

    class Parse(object):
    """
    标准HttpRunner脚本解析至前端 后端->前端
    """


    def __init__(self, body):
    """
    body: =>{
    "
    name": "get token with $user_agent, $os_platform, $app_version",
    "
    request": {
    "
    url": "/api/get-token",
    "
    method": "POST",
    "
    headers": {
    "
    app_version": "$app_version",
    "
    os_platform": "$os_platform",
    "
    user_agent": "$user_agent"
    },
    "
    json": {
    "
    sign": "${get_sign($user_agent, $device_sn, $os_platform, $app_version)}"
    },
    "
    extract": [
    {"
    token": "content.token"}
    ],
    "
    validate": [
    {"
    eq": ["status_code", 200]},
    {"
    eq": ["headers.Content-Type", "application/json"]},
    {"
    eq": ["content.success", true]}
    ],
    "
    setup_hooks": [],
    "
    teardown_hooks": []
    }
    """

    self.name = body.get('name', '')
    self.times = body.get('times', 1) # 如果导入没有times 默认为1
    self.request = body.get('request') # header files params json data
    self.variables = body.get('variables')
    self.extract = body.get('extract')
    self.validate = body.get('validate')
    self.setup_hooks = body.get('setup_hooks', [])
    self.teardown_hooks = body.get('teardown_hooks', [])
    self.desc = body.get('desc')

    self.testcase = None

    @staticmethod
    def __get_type(content):
    """
    返回data_type 默认string
    """

    var_type = {
    "str": 1,
    "int": 2,
    "float": 3,
    "bool": 4,
    "list": 5,
    "dict": 6,
    }

    key = str(type(content).__name__)

    if key in ["list", "dict"]:
    content = json.dumps(content)
    else:
    content = str(content)
    return var_type[key], content

    def parse_http(self):
    """
    标准前端脚本格式
    """

    init = {
    "key": "",
    "value": "",
    "desc": ""
    }

    init_p = {
    "key": "",
    "value": "",
    "desc": "",
    "type": 1
    }

    # 初始化test结构
    test = {
    "name": self.name,
    "times": self.times,
    "url": self.request['url'],
    "method": self.request['method'],
    "header": [init],
    "request": {
    "data": [init_p],
    "params": [init_p],
    "json_data": ''
    },
    "validate": [{
    "expect": "",
    "actual": "",
    "comparator": "equals",
    "type": 1
    }],
    "variables": [init_p],
    "extract": [init],
    "hooks": [{
    "setup": "",
    "teardown": ""
    }]
    }

    if self.request.get('headers'):
    test["header"] = []
    for key, value in self.request.pop('headers').items():
    test['header'].append({
    "key": key,
    "value": value,
    "desc": self.desc["header"][key]
    })

    if self.request.get('data'):
    test["request"]["data"] = []
    for key, value in self.request.pop('data').items():
    obj = Parse.__get_type(value)

    test['request']['data'].append({
    "key": key,
    "value": obj[1],
    "type": obj[0],
    "desc": self.desc["data"][key]
    })

    if self.request.get('params'):
    test["request"]["params"] = []
    for key, value in self.request.pop('params').items():
    test['request']['params'].append({
    "key": key,
    "value": value,
    "type": 1,
    "desc": self.desc["params"][key]
    })

    if self.request.get('json'):
    test["request"]["json_data"] = \
    json.dumps(self.request.pop("json"), indent=4,
    separators=(',', ': '), ensure_ascii=False)

    if self.extract:
    test["extract"] = []
    for content in self.extract:
    for key, value in content.items():
    test['extract'].append({
    "key": key,
    "value": value,
    "desc": self.desc["extract"][key]
    })

    if self.variables:
    test["variables"] = []
    for content in self.variables:
    for key, value in content.items():
    obj = Parse.__get_type(value)
    test["variables"].append({
    "key": key,
    "value": obj[1],
    "desc": self.desc["variables"][key],
    "type": obj[0]
    })

    if self.validate:
    test["validate"] = []
    for content in self.validate:
    for key, value in content.items():
    obj = Parse.__get_type(value[1])
    test["validate"].append({
    "expect": obj[1],
    "actual": value[0],
    "comparator": key,
    "type": obj[0]
    })

    if self.setup_hooks or self.teardown_hooks:
    test["hooks"] = []
    if len(self.setup_hooks) > len(self.teardown_hooks):
    for index in range(0, len(self.setup_hooks)):
    teardown = ""
    if index < len(self.teardown_hooks):
    teardown = self.teardown_hooks[index]
    test["hooks"].append({
    "setup": self.setup_hooks[index],
    "teardown": teardown
    })
    else:
    for index in range(0, len(self.teardown_hooks)):
    setup = ""
    if index < len(self.setup_hooks):
    setup = self.setup_hooks[index]
    test["hooks"].append({
    "setup": setup,
    "teardown": self.teardown_hooks[index]
    })

    self.testcase = test

页面交互

为了方便解析标准HttpRunner脚本结构和平台化,脚本以项目为单位管理,分为debugtalk.py, 自动化测试, 环境管理, API管理

  1. debugtalk.py即一个项目的驱动库,当然要支持在线编写调试
  2. API管理,此API并非HttpRunner 格式,其实录入的还是test结构体,因为用例是api组成,而且httprunner的业务用例是一个个test组成,所以我们后面可以页面拖拽api组成用例集,可以理解为一个yaml文件
  3. 自动化测试就是挑选API组成测试用例集了,拖拽接口,至此拖拽排序
  4. 环境管理,其实就是httprunner的config结构体,目前打算是运行时选择环境,不在用例集里面引入

实际页面

1. 项目列表页面

2. 一个小小的数据库管理工具

3.项目结构和概况

4.debugtalk.py在线编辑和调试

5.API管理,采用类似postman的文件夹形式,支持无限级目录

6.标准HttpRunner脚本编辑页面,类型支持比较完善了,这里给几张小图





7.自动化测试,拖拽API组成测试用例集

8.环境管理

还没开始做呢!

结束语

以上就是目前完成的部分内容,核心就是先录入api(实质是test结构),然后可以拖拽api组成用例集,httprunner分为testcase-api-suite,suite我一直不是很理解,所以暂时没有实现;希望各位HttpRunner天使用户们多多指导

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 16 条回复 时间 点赞

前排占座 就等他了!

SuperRunner蠢蠢欲动

大佬,等着你呢!

齐振鋆 回复

SuperRunner😳

超级瞄准已部署,坐等

辛苦,坐等开源

期待期待!

巧了,目前我也在造这个轮子···· 后端是golang,前端一样的框架
ps:前端是eolinker的体验啊

weigun 回复

嗯(⊙_⊙)前端模仿的,自己做可能界面交互就不友好了

很有意思,坚持不懈将会有大收获,向你学习

赞!看来我这文档得抓紧更新了。

😀 不错不错哦

期待ing!

debugtalk 回复

这次我一定要占个沙发😄

赞!目前已经开源了吗

需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up