前言
之前寫過(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ò)充以下功能:
- 增加配置翘单,baseurl定铜,請(qǐng)求默認(rèn)配置矮锈,用戶自定義變量
- 步驟增加,skip跳過(guò)控制钠惩,times循環(huán)控制
- 步驟中支持直接$變量名引用環(huán)境變量柒凉,及響應(yīng)文本response_text,響應(yīng)頭篓跛,response_headers, 狀態(tài)碼status_code膝捞,響應(yīng)時(shí)間response_time等。
- 使用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的隨處可用變量。 處理方式先將字典格式的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