實(shí)現(xiàn)類似httprunner的接口框架續(xù)

前言

之前寫過(guò)一個(gè)簡(jiǎn)單的httprunner的實(shí)現(xiàn):30行左右代碼實(shí)現(xiàn)一個(gè)類似httprunner的接口框架
使用Python的string.Template()來(lái)替換$變量,使用Python表達(dá)式來(lái)處理變量提取和響應(yīng)斷言喊递。功能上只實(shí)現(xiàn)了核心的接口的順序請(qǐng)求及變量的提取和斷言箫章。
這里對(duì)其功能進(jìn)行擴(kuò)充以下功能:

  1. 增加配置翘单,baseurl定铜,請(qǐng)求默認(rèn)配置矮锈,用戶自定義變量
  2. 步驟增加,skip跳過(guò)控制钠惩,times循環(huán)控制
  3. 步驟中支持直接$變量名引用環(huán)境變量柒凉,及響應(yīng)文本response_text,響應(yīng)頭篓跛,response_headers, 狀態(tài)碼status_code膝捞,響應(yīng)時(shí)間response_time等。
  4. 使用Session會(huì)話維持愧沟,根據(jù)request字典是否包含data/json/files蔬咬,設(shè)置默認(rèn)請(qǐng)求方法

Yaml數(shù)據(jù)文件格式

  • config:配置
    • baseurl:接口域名端口配置
    • request:請(qǐng)求默認(rèn)配置,如默認(rèn)headers, timeout等
    • variables:用戶自定義變量
  • tests:測(cè)試步驟
    • name: 步驟名稱
    • skip: 是否跳過(guò)
    • times: 循環(huán)次數(shù)
    • request: 請(qǐng)求數(shù)據(jù)沐寺,對(duì)應(yīng)requests.request()方法的參數(shù)
    • extact: 提取變量林艘,Python表達(dá)式,使用的eval()計(jì)算芽丹,存儲(chǔ)到上下文context字典變量中
    • verify: 斷言北启,Python表達(dá)式,使用eval()計(jì)算

示例數(shù)據(jù)data.yaml如下:

config:
  name: '測(cè)試用例'
  request:
    timeout: 30
    headers:
      x-test: abc123
  variables: 
    client_id: kPoFYw85FXsnojsy5bB9hu6x
    client_secret: &client_secret l7SuGBkDQHkjiTPU3m6NaNddD6SCvDMC

tests:
  - name: 步驟1-獲取百度token接口 # 接口名稱
    request:  # 請(qǐng)求報(bào)文
      url: https://aip.baidubce.com/oauth/2.0/token
      params:
        grant_type: client_credentials
        client_id: $client_id
        client_secret: *client_secret  # 使用錨點(diǎn)
    extract:  # 提取變量, 字典格式
      token:  response.json()['access_token']  # RESPONSE系統(tǒng)變量拔第,代表響應(yīng)對(duì)象
    verify:
      - status_code == 200
  - name: 步驟2-百度ORC接口  # 第二個(gè)接口
    request:
      url: https://aip.baidubce.com/rest/2.0/ocr/v1/general_basic?access_token=${token}  # 使用變量
      data:  # 請(qǐng)求體(表單格式)
        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-跳過(guò)
    skip: True
  - name: 步驟4-重復(fù)執(zhí)行
    times: 3
    request:
      url: https://httpbin.org/get

注:由于yaml語(yǔ)法中自帶錨點(diǎn)功能咕村,如配置variables中client_secret即設(shè)置了錨點(diǎn),引用也非常方便蚊俺。因此variables的也可以不使用模板替換懈涛,直接使用yaml的錨點(diǎn)引用。

實(shí)現(xiàn)步驟

關(guān)鍵字定義

不同的設(shè)計(jì)者對(duì)字段喜歡用不同的關(guān)鍵字泳猬,如Robot Framework中最外層使用settings/testcases/variables/keywords批钠,httprunner中config/test,Jenkins Pipelines中使用options/stages等得封。
這里對(duì)使用的關(guān)鍵字進(jìn)行了定義埋心,讀者也可以改為自己使用的關(guān)鍵字。

CONFIG = 'config'  # 配置關(guān)鍵字  settings
STEPS = 'tests'  # 步驟關(guān)鍵字  steps/teststeps/testcases

NAME = 'name'  # 名稱
VAIABLES = 'variables'  # 用戶自定義變量關(guān)鍵字
BASEURL = 'baseurl'
REQUEST = 'request'  # 請(qǐng)求配置,請(qǐng)求數(shù)據(jù)關(guān)鍵字
CHECK = 'verify'  # 驗(yàn)證關(guān)鍵字  check/validate/assert
EXTRACT = 'extract'   # 提取關(guān)鍵字 output/register
SKIP = 'skip'  # 跳過(guò)步驟關(guān)鍵字
TIMES = 'times'  # 循環(huán)步驟關(guān)鍵字  circle

配置解析

首先我們使用requests.session()建立一個(gè)會(huì)話忙上,會(huì)話可以保持登錄等請(qǐng)求狀態(tài)拷呆,并可以對(duì)其設(shè)置默認(rèn)請(qǐng)求參數(shù)。

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配置疫粥,則將字典格式的配置信息茬斧,添加為會(huì)話對(duì)象session的屬性。

上下文變量

上下文變量是保存用戶自定義變量梗逮,環(huán)境變量项秉,用戶提取的變量,和響應(yīng)的一些變量的慷彤。之前直接使用的locals()即當(dāng)前局部變量娄蔼。這里新建一個(gè)專用的變量context怖喻。由于包含多個(gè)部分的內(nèi)容,這里可以使用Python的ChainMap岁诉,導(dǎo)入方式為from collections import ChainMap罢防,也可以直接使用字典格式,使用update更新值唉侄。

context = ChainMap(variables, os.environ)

vaiables是用戶自定義變量,os.environ是環(huán)境變量野建,ChaInMap類似一種聯(lián)合字典属划,逐個(gè)字典查找需要的鍵值,更新時(shí)變量更新到第一個(gè)字典中候生。

步驟解析

步驟對(duì)應(yīng)data.yaml中的tests段同眯,格式是一個(gè)列表。
使用循環(huán)唯鸭,遍歷執(zhí)行每一個(gè)步驟须蜗,如果步驟中設(shè)置了skip則跳過(guò)。執(zhí)行是times次數(shù)循環(huán)執(zhí)行步驟目溉。

context['steps'] = []  # 用于保存所有步驟的請(qǐng)求和響應(yīng)明肮,便于跨步驟引用
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(' 跳過(guò)步驟:', step_name)
        continue

    for i in range(times):
        print(' 執(zhí)行步驟:', step_name, f'第{i+1}輪' if step.get(TIMES) else '')

打印步驟時(shí),如果包含times字段則在步驟后輸出第幾輪缭付。

請(qǐng)求變量解析

不同于httprunner的隨處可用引用變量或函數(shù)柿估,這里限定只允許在請(qǐng)求數(shù)據(jù)request中使用`變量。 處理方式先將字典格式的request陷猫,轉(zhuǎn)為yaml字符串秫舌。這里不使用默認(rèn)的yaml流格式,因?yàn)閥aml流格式將字典轉(zhuǎn)為{a: 1, b:2}而不是a: 1\nb: 2`绣檬,大括號(hào)對(duì)模板變量替換有一些影響足陨。

request_str = yaml.dump(request, default_flow_style=False)  # 先轉(zhuǎn)為字符串
if '$' in request_str:
    request_str = Template(request_str).safe_substitute(context)  # 替換${變量}為varables中的同名變量
request = yaml.safe_load(request_str)  # 重新轉(zhuǎn)為字典

設(shè)置默認(rèn)請(qǐng)求方法

由于requests.request()方法中method參數(shù)是必選參數(shù),因此每個(gè)請(qǐng)求段都必選有method字段娇未,但是筆者卻總是忘記寫墨缘。
這了為了可以不寫method,對(duì)其添加默認(rèn)值忘蟹,如果請(qǐng)求字段中有data/json/files字段飒房,則默認(rèn)使用post,否則默認(rèn)使用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默認(rèn)不帶/狠毯,而url以/開頭。為避免少些或多寫/褥芒,也可以使用request['url'] = '/'.join((baseurl.rstrip('/'), url.lstrip('/')))

發(fā)送請(qǐng)求

發(fā)送請(qǐng)求時(shí)直接將請(qǐng)求數(shù)據(jù)request字典嚼松,解包放入session.request()方法中即可嫡良,注意request字典中不能有該方法不支持的參數(shù)。

print('  請(qǐng)求url:', request.get('url'))  # print(' 發(fā)送請(qǐng)求:', request)
response = session.request(**request)  # 字典解包献酗,發(fā)送接口
print('  狀態(tài)碼:', response.status_code)  # print(' 響應(yīng)數(shù)據(jù):', response.text)
# 注冊(cè)上下文變量
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)  # 保存步驟結(jié)果
context.update(step_result)  # 將最近的響應(yīng)結(jié)果更新到上下文變量中

這里將響應(yīng)的一些數(shù)據(jù)組成字典添加到上下文變量中寝受,這里添加了兩次。
第一次是將本步驟的結(jié)果添加到上下文的steps罕偎,這里保存了每一步的結(jié)果很澄。
第二次是將本次請(qǐng)求的request,response等變量直接添加到上下文中,即最近一次請(qǐng)求的請(qǐng)求和響應(yīng)結(jié)果颜及,方便提取或斷言中可以直接使用這些變量甩苛。

變量提取及處理斷言

# 提取變量
extract = step.get(EXTRACT)
if extract is not None:  # 如果存在extract
    for key, value in extract.items():
        print("  提取變量:", key, value)
        # 計(jì)算value表達(dá)式,可使用的全局變量為空俏站,可使用的局部變量為上下文context中的變量
        context[key] = eval(value, {}, context)  # 保存變量結(jié)果到局部變量中
# 處理斷言
check = step.get(CHECK)
if check and isinstance(check, list):
    for line in check:
        result = eval(line, {}, context)  # 計(jì)算斷言表達(dá)式讯蒲,True代表成功,F(xiàn)alse代表失敗
        print("  處理斷言:", line, "結(jié)果:", "PASS" if result else "FAIL") 

extact段為一個(gè)字典肄扎,key為要保存的變量名墨林,value是一個(gè)Python表達(dá)式字符串,這里使用eval()執(zhí)行Python表達(dá)式犯祠,將返回的值注冊(cè)到上下文context變量中旭等。
eval()由于可以直接將字符串按Python語(yǔ)句執(zhí)行,是存在安全隱患的雷则,在使用eval()時(shí)辆雾,應(yīng)盡量限定其使用的全局變量和局部變量。這里限定eval解析時(shí)只運(yùn)行使用context上下文中的變量月劈。

完整代碼

import os
from string import Template
from collections import ChainMap

import yaml
import requests

# 步驟定義
CONFIG = 'config'  # 配置關(guān)鍵字  settings
STEPS = 'tests'  # 步驟關(guān)鍵字  steps/teststeps/testcases

NAME = 'name'  # 名稱
VAIABLES = 'variables'  # 用戶自定義變量關(guān)鍵字
BASEURL = 'baseurl'
REQUEST = 'request'  # 請(qǐng)求配置,請(qǐng)求數(shù)據(jù)關(guān)鍵字
CHECK = 'verify'  # 驗(yàn)證關(guān)鍵字  check/validate/assert
EXTRACT = 'extract'   # 提取關(guān)鍵字 output/register
SKIP = 'skip'  # 跳過(guò)步驟關(guān)鍵字
TIMES = 'times'  # 循環(huán)步驟關(guān)鍵字  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('執(zhí)行用例:', name)

    # 上下文變量
    context = ChainMap(variables, os.environ)
    # 解析步驟
    context['steps'] = []  # 用于保存所有步驟的請(qǐng)求和響應(yīng), 便于跨步驟引用
    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(' 跳過(guò)步驟:', step_name)
            continue

        for i in range(times):
            print(' 執(zhí)行步驟:', step_name, f'第{i+1}輪' if step.get(TIMES) else '')
            # 請(qǐng)求$變量解析
            if not request:
                continue
            request_str = yaml.dump(request, default_flow_style=False)  # 先轉(zhuǎn)為字符串
            if '$' in request_str:
                request_str = Template(request_str).safe_substitute(context)  # 替換${變量}為varables中的同名變量
                request = yaml.safe_load(request_str)  # 重新轉(zhuǎn)為字典
            # 設(shè)置默認(rèn)請(qǐng)求方法
            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('/')))

            # 發(fā)送請(qǐng)求
            print('  請(qǐng)求url:', request.get('url'))  # print(' 發(fā)送請(qǐng)求:', request)
            response = session.request(**request)  # 字典解包度迂,發(fā)送接口
            print('  狀態(tài)碼:', response.status_code)  # print(' 響應(yīng)數(shù)據(jù):', response.text)

            # 注冊(cè)上下文變量
            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)  # 保存步驟結(jié)果
            context.update(step_result)  # 將最近的響應(yīng)結(jié)果更新到上下文變量中

            # 提取變量
            extract = step.get(EXTRACT)
            if extract is not None:  # 如果存在extract
                for key, value in extract.items():
                    print("  提取變量:", key, value)
                    # 計(jì)算value表達(dá)式,可使用的全局變量為空猜揪,可使用的局部變量為RESPONSE(響應(yīng)對(duì)象)
                    context[key] = eval(value, {}, context)  # 保存變量結(jié)果到上下文中
            # 處理斷言
            check = step.get(CHECK)
            if check and isinstance(check, list):
                for line in check:
                    result = eval(line, {}, context)  # 計(jì)算斷言表達(dá)式惭墓,True代表成功,F(xiàn)alse代表失敗
                    print("  處理斷言:", line, "結(jié)果:", "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)

執(zhí)行結(jié)果:

執(zhí)行用例: 測(cè)試用例
 執(zhí)行步驟: 步驟1-獲取百度token接口 
  請(qǐng)求url: https://aip.baidubce.com/oauth/2.0/token
  狀態(tài)碼: 200
  提取變量: token response.json()['access_token']
  處理斷言: status_code == 200 結(jié)果: PASS
 執(zhí)行步驟: 步驟2-百度ORC接口 
  請(qǐng)求url: https://aip.baidubce.com/rest/2.0/ocr/v1/general_basic?access_token=24.07107d04b4252ecead3c2be01cf52613.2592000.1587199134.282335-11767296
  狀態(tài)碼: 200
  處理斷言: response.json()['words_result_num'] == 6 結(jié)果: PASS
 跳過(guò)步驟: 步驟3-跳過(guò)
 執(zhí)行步驟: 步驟4-重復(fù)執(zhí)行 第1輪
  請(qǐng)求url: https://httpbin.org/get
  狀態(tài)碼: 200
 執(zhí)行步驟: 步驟4-重復(fù)執(zhí)行 第2輪
  請(qǐng)求url: https://httpbin.org/get
  狀態(tài)碼: 200
 執(zhí)行步驟: 步驟4-重復(fù)執(zhí)行 第3輪
  請(qǐng)求url: https://httpbin.org/get
  狀態(tài)碼: 200
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末而姐,一起剝皮案震驚了整個(gè)濱河市腊凶,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌拴念,老刑警劉巖钧萍,帶你破解...
    沈念sama閱讀 217,406評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異政鼠,居然都是意外死亡风瘦,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門公般,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)万搔,“玉大人胡桨,你說(shuō)我怎么就攤上這事∷脖ⅲ” “怎么了昧谊?”我有些...
    開封第一講書人閱讀 163,711評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)酗捌。 經(jīng)常有香客問(wèn)我呢诬,道長(zhǎng),這世上最難降的妖魔是什么胖缤? 我笑而不...
    開封第一講書人閱讀 58,380評(píng)論 1 293
  • 正文 為了忘掉前任馅巷,我火速辦了婚禮,結(jié)果婚禮上草姻,老公的妹妹穿的比我還像新娘。我一直安慰自己稍刀,他們只是感情好撩独,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著账月,像睡著了一般综膀。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上局齿,一...
    開封第一講書人閱讀 51,301評(píng)論 1 301
  • 那天剧劝,我揣著相機(jī)與錄音,去河邊找鬼抓歼。 笑死讥此,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的谣妻。 我是一名探鬼主播萄喳,決...
    沈念sama閱讀 40,145評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼蹋半!你這毒婦竟也來(lái)了他巨?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,008評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤减江,失蹤者是張志新(化名)和其女友劉穎染突,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體辈灼,經(jīng)...
    沈念sama閱讀 45,443評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡份企,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評(píng)論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了茵休。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片薪棒。...
    茶點(diǎn)故事閱讀 39,795評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡手蝎,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出俐芯,到底是詐尸還是另有隱情棵介,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評(píng)論 5 345
  • 正文 年R本政府宣布吧史,位于F島的核電站邮辽,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏贸营。R本人自食惡果不足惜吨述,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望钞脂。 院中可真熱鬧揣云,春花似錦、人聲如沸冰啃。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)阎毅。三九已至焚刚,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間扇调,已是汗流浹背矿咕。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留狼钮,地道東北人碳柱。 一個(gè)月前我還...
    沈念sama閱讀 47,899評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像熬芜,于是被迫代替她去往敵國(guó)和親士聪。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容