• 实现类似httprunner的接口框架续


    前言

    之前写过一个简单的httprunner的实现:30行左右代码实现一个类似httprunner的接口框架
    使用Python的string.Template()来替换$变量,使用Python表达式来处理变量提取和响应断言。功能上只实现了核心的接口的顺序请求及变量的提取和断言。
    这里对其功能进行扩充以下功能:

    1. 增加配置,baseurl,请求默认配置,用户自定义变量
    2. 步骤增加,skip跳过控制,times循环控制
    3. 步骤中支持直接$变量名引用环境变量,及响应文本response_text,响应头,response_headers, 状态码status_code,响应时间response_time等。
    4. 使用Session会话维持,根据request字典是否包含data/json/files,设置默认请求方法

    Yaml数据文件格式

    • config:配置
      • baseurl:接口域名端口配置
      • request:请求默认配置,如默认headers, timeout等
      • variables:用户自定义变量
    • tests:测试步骤
      • name: 步骤名称
      • skip: 是否跳过
      • times: 循环次数
      • request: 请求数据,对应requests.request()方法的参数
      • extact: 提取变量,Python表达式,使用的eval()计算,存储到上下文context字典变量中
      • verify: 断言,Python表达式,使用eval()计算

    示例数据data.yaml如下:

    config:
      name: '测试用例'
      request:
        timeout: 30
        headers:
          x-test: abc123
      variables: 
        client_id: kPoFYw85FXsnojsy5bB9hu6x
        client_secret: &client_secret l7SuGBkDQHkjiTPU3m6NaNddD6SCvDMC
    
    tests:
      - name: 步骤1-获取百度token接口 # 接口名称
        request:  # 请求报文
          url: https://aip.baidubce.com/oauth/2.0/token
          params:
            grant_type: client_credentials
            client_id: $client_id
            client_secret: *client_secret  # 使用锚点
        extract:  # 提取变量, 字典格式
          token:  response.json()['access_token']  # RESPONSE系统变量,代表响应对象
        verify:
          - status_code == 200
      - name: 步骤2-百度ORC接口  # 第二个接口
        request:
          url: https://aip.baidubce.com/rest/2.0/ocr/v1/general_basic?access_token=${token}  # 使用变量
          data:  # 请求体(表单格式)
            url: https://upload-images.jianshu.io/upload_images/7575721-40c847532432e852.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240
        verify:  # 断言, 列表格式
          - response.json()['words_result_num'] == 6
      - name: 步骤3-跳过
        skip: True
      - name: 步骤4-重复执行
        times: 3
        request:
          url: https://httpbin.org/get
    

    注:由于yaml语法中自带锚点功能,如配置variables中client_secret即设置了锚点,引用也非常方便。因此variables的也可以不使用模板替换,直接使用yaml的锚点引用。

    实现步骤

    关键字定义

    不同的设计者对字段喜欢用不同的关键字,如Robot Framework中最外层使用settings/testcases/variables/keywords,httprunner中config/test,Jenkins Pipelines中使用options/stages等。
    这里对使用的关键字进行了定义,读者也可以改为自己使用的关键字。

    CONFIG = 'config'  # 配置关键字  settings
    STEPS = 'tests'  # 步骤关键字  steps/teststeps/testcases
    
    NAME = 'name'  # 名称
    VAIABLES = 'variables'  # 用户自定义变量关键字
    BASEURL = 'baseurl'
    REQUEST = 'request'  # 请求配置,请求数据关键字
    CHECK = 'verify'  # 验证关键字  check/validate/assert
    EXTRACT = 'extract'   # 提取关键字 output/register
    SKIP = 'skip'  # 跳过步骤关键字
    TIMES = 'times'  # 循环步骤关键字  circle
    

    配置解析

    首先我们使用requests.session()建立一个会话,会话可以保持登录等请求状态,并可以对其设置默认请求参数。

    session = requests.session()
    config = data.get(CONFIG)
    if config:
        name = config.get(NAME)
        variables = config.get(VAIABLES, {})
        baseurl = config.get(BASEURL)
        request = config.get(REQUEST)
        if request:
            for key, value in request.items():
                session.__setattr__(key, value) 
    

    如果存在request配置,则将字典格式的配置信息,添加为会话对象session的属性。

    上下文变量

    上下文变量是保存用户自定义变量,环境变量,用户提取的变量,和响应的一些变量的。之前直接使用的locals()即当前局部变量。这里新建一个专用的变量context。由于包含多个部分的内容,这里可以使用Python的ChainMap,导入方式为from collections import ChainMap,也可以直接使用字典格式,使用update更新值。

    context = ChainMap(variables, os.environ)
    

    vaiables是用户自定义变量,os.environ是环境变量,ChaInMap类似一种联合字典,逐个字典查找需要的键值,更新时变量更新到第一个字典中。

    步骤解析

    步骤对应data.yaml中的tests段,格式是一个列表。
    使用循环,遍历执行每一个步骤,如果步骤中设置了skip则跳过。执行是times次数循环执行步骤。

    context['steps'] = []  # 用于保存所有步骤的请求和响应,便于跨步骤引用
    steps = data.get(STEPS)
    for step in steps:
        step_name = step.get(NAME)
        skip = step.get(SKIP)
        times = step.get(TIMES, 1)
        request = step.get(REQUEST)
        if skip or not request:
            print(' 跳过步骤:', step_name)
            continue
    
        for i in range(times):
            print(' 执行步骤:', step_name, f'第{i+1}轮' if step.get(TIMES) else '')
    

    打印步骤时,如果包含times字段则在步骤后输出第几轮。

    请求变量解析

    不同于httprunner的随处可用(引用变量或函数,这里限定只允许在请求数据request中使用`)变量。 处理方式先将字典格式的request,转为yaml字符串。这里不使用默认的yaml流格式,因为yaml流格式将字典转为{a: 1, b:2}而不是a: 1 b: 2`,大括号对模板变量替换有一些影响。

    request_str = yaml.dump(request, default_flow_style=False)  # 先转为字符串
    if '$' in request_str:
        request_str = Template(request_str).safe_substitute(context)  # 替换${变量}为varables中的同名变量
    request = yaml.safe_load(request_str)  # 重新转为字典
    

    设置默认请求方法

    由于requests.request()方法中method参数是必选参数,因此每个请求段都必选有method字段,但是笔者却总是忘记写。
    这了为了可以不写method,对其添加默认值,如果请求字段中有data/json/files字段,则默认使用post,否则默认使用get。

    if request.get('data') or request.get('json') or request.get('files'):
       request.setdefault('method', 'post')
    else:
       request.setdefault('method', 'get')
    

    组装baseurl

    if baseurl:
        url = request.get('url')
        if not url.startswith('http'):
            request['url'] = base_url + url
    

    通常情况下baseurl默认不带/,而url以/开头。为避免少些或多写/,也可以使用request['url'] = '/'.join((baseurl.rstrip('/'), url.lstrip('/')))

    发送请求

    发送请求时直接将请求数据request字典,解包放入session.request()方法中即可,注意request字典中不能有该方法不支持的参数。

    print('  请求url:', request.get('url'))  # print(' 发送请求:', request)
    response = session.request(**request)  # 字典解包,发送接口
    print('  状态码:', response.status_code)  # print(' 响应数据:', response.text)
    # 注册上下文变量
    step_result = dict(
        request=request,
        response=response,
        status_code=response.status_code,
        response_text=response.text,
        response_headers=response.headers,
        response_time=response.elapsed.seconds
    )
    context['steps'].append(step_result)  # 保存步骤结果
    context.update(step_result)  # 将最近的响应结果更新到上下文变量中
    

    这里将响应的一些数据组成字典添加到上下文变量中,这里添加了两次。
    第一次是将本步骤的结果添加到上下文的steps,这里保存了每一步的结果。
    第二次是将本次请求的request,response等变量直接添加到上下文中,即最近一次请求的请求和响应结果,方便提取或断言中可以直接使用这些变量。

    变量提取及处理断言

    # 提取变量
    extract = step.get(EXTRACT)
    if extract is not None:  # 如果存在extract
        for key, value in extract.items():
            print("  提取变量:", key, value)
            # 计算value表达式,可使用的全局变量为空,可使用的局部变量为上下文context中的变量
            context[key] = eval(value, {}, context)  # 保存变量结果到局部变量中
    # 处理断言
    check = step.get(CHECK)
    if check and isinstance(check, list):
        for line in check:
            result = eval(line, {}, context)  # 计算断言表达式,True代表成功,False代表失败
            print("  处理断言:", line, "结果:", "PASS" if result else "FAIL") 
    

    extact段为一个字典,key为要保存的变量名,value是一个Python表达式字符串,这里使用eval()执行Python表达式,将返回的值注册到上下文context变量中。
    eval()由于可以直接将字符串按Python语句执行,是存在安全隐患的,在使用eval()时,应尽量限定其使用的全局变量和局部变量。这里限定eval解析时只运行使用context上下文中的变量。

    完整代码

    import os
    from string import Template
    from collections import ChainMap
    
    import yaml
    import requests
    
    # 步骤定义
    CONFIG = 'config'  # 配置关键字  settings
    STEPS = 'tests'  # 步骤关键字  steps/teststeps/testcases
    
    NAME = 'name'  # 名称
    VAIABLES = 'variables'  # 用户自定义变量关键字
    BASEURL = 'baseurl'
    REQUEST = 'request'  # 请求配置,请求数据关键字
    CHECK = 'verify'  # 验证关键字  check/validate/assert
    EXTRACT = 'extract'   # 提取关键字 output/register
    SKIP = 'skip'  # 跳过步骤关键字
    TIMES = 'times'  # 循环步骤关键字  circle
    
    
    def run(data):
        # 解析配置
        session = requests.session()
        config = data.get(CONFIG)
        if config:
            name = config.get(NAME)
            variables = config.get(VAIABLES, {})
            baseurl = config.get(BASEURL)
            request = config.get(REQUEST)
            if request:
                for key, value in request.items():
                    session.__setattr__(key, value)
            print('执行用例:', name)
    
        # 上下文变量
        context = ChainMap(variables, os.environ)
        # 解析步骤
        context['steps'] = []  # 用于保存所有步骤的请求和响应, 便于跨步骤引用
        steps = data.get(STEPS) 
        for step in steps:
            step_name = step.get(NAME)
            skip = step.get(SKIP)
            times = step.get(TIMES, 1)
            request = step.get(REQUEST)
            if skip or not request:
                print(' 跳过步骤:', step_name)
                continue
    
            for i in range(times):
                print(' 执行步骤:', step_name, f'第{i+1}轮' if step.get(TIMES) else '')
                # 请求$变量解析
                if not request:
                    continue
                request_str = yaml.dump(request, default_flow_style=False)  # 先转为字符串
                if '$' in request_str:
                    request_str = Template(request_str).safe_substitute(context)  # 替换${变量}为varables中的同名变量
                    request = yaml.safe_load(request_str)  # 重新转为字典
                # 设置默认请求方法
                if request.get('data') or request.get('json') or request.get('files'):
                    request.setdefault('method', 'post')
                else:
                    request.setdefault('method', 'get')
                # 组装baseurl
                if baseurl:
                    url = request.get('url')
                    if not url.startswith('http'):
                        request['url'] = '/'.join((baseurl.rstrip('/'), url.lstrip('/')))
    
                # 发送请求
                print('  请求url:', request.get('url'))  # print(' 发送请求:', request)
                response = session.request(**request)  # 字典解包,发送接口
                print('  状态码:', response.status_code)  # print(' 响应数据:', response.text)
    
                # 注册上下文变量
                step_result = dict(
                    request=request,
                    response=response,
                    status_code=response.status_code,
                    response_text=response.text,
                    response_headers=response.headers,
                    response_time=response.elapsed.seconds
                )
                context['steps'].append(step_result)  # 保存步骤结果
                context.update(step_result)  # 将最近的响应结果更新到上下文变量中
    
                # 提取变量
                extract = step.get(EXTRACT)
                if extract is not None:  # 如果存在extract
                    for key, value in extract.items():
                        print("  提取变量:", key, value)
                        # 计算value表达式,可使用的全局变量为空,可使用的局部变量为RESPONSE(响应对象)
                        context[key] = eval(value, {}, context)  # 保存变量结果到上下文中
                # 处理断言
                check = step.get(CHECK)
                if check and isinstance(check, list):
                    for line in check:
                        result = eval(line, {}, context)  # 计算断言表达式,True代表成功,False代表失败
                        print("  处理断言:", line, "结果:", "PASS" if result else "FAIL")
        return context['steps']
    
    
    if __name__ == "__main__":
        with open('data.yml', encoding='utf-8') as f:
            data = yaml.safe_load(f)
        run(data)
    

    执行结果:

    执行用例: 测试用例
     执行步骤: 步骤1-获取百度token接口 
      请求url: https://aip.baidubce.com/oauth/2.0/token
      状态码: 200
      提取变量: token response.json()['access_token']
      处理断言: status_code == 200 结果: PASS
     执行步骤: 步骤2-百度ORC接口 
      请求url: https://aip.baidubce.com/rest/2.0/ocr/v1/general_basic?access_token=24.07107d04b4252ecead3c2be01cf52613.2592000.1587199134.282335-11767296
      状态码: 200
      处理断言: response.json()['words_result_num'] == 6 结果: PASS
     跳过步骤: 步骤3-跳过
     执行步骤: 步骤4-重复执行 第1轮
      请求url: https://httpbin.org/get
      状态码: 200
     执行步骤: 步骤4-重复执行 第2轮
      请求url: https://httpbin.org/get
      状态码: 200
     执行步骤: 步骤4-重复执行 第3轮
      请求url: https://httpbin.org/get
      状态码: 200
    
  • 相关阅读:
    Python成长笔记
    Python成长笔记
    Python成长笔记
    Python成长笔记
    Python成长笔记
    Python成长笔记
    Python成长笔记
    Python成长笔记
    Python成长笔记
    解决Jenkins生成测试报告的问题
  • 原文地址:https://www.cnblogs.com/superhin/p/12525278.html
Copyright © 2020-2023  润新知