在开发测试中经常会遇到接口迭代和代码重构,一个无关紧要的改动往往会引起整个项目的运行。现有的接口测试中往往只是针对单一接口的测试,可是业务的连贯性是非常紧密的,比如:用户从登陆,获取商品信息,下单,支付回调的处理,以及退款等流程的处理。发现没有好使的接口测试工具(jmeter太难用了)。于是自己使用python 实现自动化接口测试的一套脚本,该脚本可以实现单一接口的测试,和流程的测试;支持多个项目之间的测试;主要的功能如下:
- 读取Excel
- 接口参数(1、地址栏参数,2、body参数)动态化,可以从请求返回值中提取数据作为全局参数,供流程下一步骤使用
- 根据接口发送请求
- 结果的诊断,使用jsonpath
- 结果报告邮件
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import xlrd
import requests
import json
import logging
import smtplib
from email.mime.text import MIMEText
from email.utils import formataddr
import jsonpath
import sys
import traceback
#日志定义
logging.basicConfig(level=logging.DEBUG, # log level
format='[%(asctime)s] %(levelname)s [%(funcName)s: %(filename)s, %(lineno)d] %(message)s', # log格式
datefmt='%Y-%m-%d %H:%M:%S', # 日期格式
filename='log.txt', # 日志输出文件
filemode='a') # 追加模式
class Config:
global PHP_API_DOMAIN #php接口域名
global JAVA_API_DOMAIN #java接口域名
global TEST_CASE_PATH #测试用例excel的路径
global TEST_CASE_LIST #测试用例转换后的集合<字典>
global PARAMS #存储结果变量
global CURRENT_REQUEST_HEADER #存储当前请求的请求头
global CURRENT_REQUEST_PARAM #存储当前请求的请求数据
global ALL_REQUEST_RESULT
global CURRENT_REQUEST_STATUS
def __init__(self,php_api_domain,java_api_domain,test_case_path):
self.PHP_API_DOMAIN = php_api_domain
self.TEST_CASE_PATH = test_case_path
self.JAVA_API_DOMAIN = java_api_domain
self.TEST_CASE_LIST = []
self.PARAMS = {}
self.CURRENT_REQUEST_HEADER = {}
self.CURRENT_REQUEST_PARAM = {}
self.ALL_REQUEST_RESULT = []
self.CURRENT_REQUEST_STATUS = True
headers = {'Accept': 'application/json, text/javascript, */*',
'Accept-Encoding':'gzip, deflate, br',
'Accept-Language':'zh-CN,zh;q=0.9',
'Connection':'Keep-Alive',
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36 Edge/15.15063'}
class request_handler:
test_case = {}
result_obj = {}
assert_obj = {}
assert_result = True
need_assert = True
def __init__(self, test_case):
self.test_case = test_case
self.main()
self.return_entity()
def main(self):
self.request(self.test_case)
if self.need_assert:
self.setting_param()
def return_entity(self):
return_entity = {}
return_entity['成功状态'] = self.assert_result
return_entity['请求结果数据'] = self.result_obj
return_entity['断言结果'] = self.assert_obj
return_entity['用例数据'] = self.test_case
return_entity['请求参数'] = config.CURRENT_REQUEST_PARAM
return_entity['请求头'] = config.CURRENT_REQUEST_HEADER
logging.info(return_entity)
if not self.assert_result:
config.ALL_REQUEST_RESULT.append(return_entity)
config.CURRENT_REQUEST_STATUS = False
else:
config.CURRENT_REQUEST_STATUS = True
def request(self,test_case):
try:
if test_case['method'] == 'GET':
self.result_obj = self.request_get(test_case)
if test_case['method'] == 'POST':
self.result_obj = self.request_post(test_case)
if test_case['method'] == 'DELETE':
self.result_obj = self.request_delete(test_case)
if test_case['method'] == 'PUT':
self.result_obj = self.request_put(test_case)
self.assert_handler()
except Exception as e:
self.need_assert = False
self.assert_result = False
self.result_obj = {"status_code": "error", "error_message": e}
traceback.print_exc()
info = traceback.format_exc()
print(info)
def request_get(self,object):
url = self.get_url(object['url'], object['type'], object['param_data'])
head = headers
if isinstance(object['header'],list):
head = self.get_config_param(object['header'], head)
request_body = {}
if isinstance(json.loads(object['body_data']),dict):
request_body = self.get_config_param(json.loads(object['body_data']), request_body)
print("GET URL:"+url)
config.CURRENT_REQUEST_HEADER = head
config.CURRENT_REQUEST_PARAM = request_body
response = requests.get(url=url, params=request_body,headers=head)
return {"status_code" : response.status_code, "response_content": response.text}
def request_delete(self,object):
url = self.get_url(object['url'], object['type'], object['param_data'])
head = headers
if isinstance(object['header'],list):
head = self.get_config_param(object['header'], head)
request_body = {}
if isinstance(json.loads(object['body_data']),dict):
request_body = self.get_config_param(json.loads(object['body_data']), request_body)
print("GET URL:"+url)
config.CURRENT_REQUEST_HEADER = head
config.CURRENT_REQUEST_PARAM = request_body
response = requests.get(url=url, params=request_body,headers=head)
return {"status_code" : response.status_code, "response_content": response.text}
def request_put(self,object):
url = self.get_url(object['url'], object['type'], object['param_data'])
head = headers
if isinstance(object['header'],list):
head = self.get_config_param(object['header'], head)
request_body = {}
if isinstance(json.loads(object['body_data']),dict):
request_body = self.get_config_param(json.loads(object['body_data']), request_body)
print("GET URL:"+url + '; 参数:' + request_body)
config.CURRENT_REQUEST_HEADER = head
config.CURRENT_REQUEST_PARAM = request_body
response = requests.get(url=url, params=request_body,headers=head)
return {"status_code" : response.status_code, "response_content": response.text}
def request_post(self,object):
head = headers
#将header中的数据添加到请求中
if isinstance(json.loads(object['header']),dict):
head = self.get_config_param(json.loads(object['header']),head)
url = self.get_url(object['url'], object['type'], object['param_data'])
#将data中的动态参数从共享参数中添加到数据中
request_body = {}
if isinstance(json.loads(object['body_data']),dict):
request_body = self.get_config_param(json.loads(object['body_data']), request_body)
print("POST URL:" + url+";param:"+str(request_body))
config.CURRENT_REQUEST_HEADER = head
config.CURRENT_REQUEST_PARAM = request_body
response = requests.post(url, request_body, headers=head)
return {"status_code": response.status_code, "response_content": response.text}
def get_config_param(self, list, object):
for param_name in list.keys():
param_value = list[param_name]
if not str(param_value).startswith("$P_"):
object[param_name] = param_value
else:
body_value = config.PARAMS[param_value] if param_value in config.PARAMS.keys() else param_value
object[param_name] = body_value
return object
def get_url(self, path, type, param_data):
request_url = (config.PHP_API_DOMAIN + '/' + path) if type == 'php' else (config.JAVA_API_DOMAIN + '/' + path)
param_data = json.loads(param_data)
url_param = self.get_config_param(param_data, {})
request_url = request_url + '?a=a'
for key in url_param.keys():
request_url = request_url + "&" + key + '=' + url_param[key]
return request_url
def assert_handler(self):
test_case = self.test_case
result_obj = self.result_obj
self.assert_obj = {"测试用例名称:": test_case['case_name']}
# 访问接口直接报错直接
if result_obj['status_code'] == 'error':
self.assert_obj = dict(self.assert_obj.items(), {"请求状态:": "错误", "错误信息:": result_obj['error_message']}.items())
self.need_assert = False
self.assert_result = False
return
# 状态大于300的异常直接处理
if result_obj['status_code'] >= 300:
self.assert_obj['请求状态'] = result_obj['status_code']
self.assert_obj['错误信息'] = result_obj['response_content']
self.need_assert = False
self.assert_result = False
return
# 请求状态为成功,判断业务状态
buiess_content = json.loads(result_obj['response_content'])
expect_res = json.loads(test_case['expect_res'])
# 校验规则
for ruler in expect_res:
matcher = jsonpath.jsonpath(buiess_content, ruler['rule'])
if isinstance(matcher,bool):
self.assert_obj['请求状态'] = result_obj['status_code']
self.assert_obj['错误信息'] = '规则:' + ruler['rule'] + '值不匹配,期望是:' + str(ruler['value']) + ',返回是:' + str(
matcher)
self.assert_result = False
break
elif len(matcher) == 0:
self.assert_obj['请求状态'] = result_obj['status_code']
self.assert_obj['错误信息'] = '规则:' + ruler['rule'] + '提取出的数组长度为0'
self.assert_result = False
break
elif ruler['type'] == 'value': # 对值进行比较
if matcher[0] == ruler['value']:
continue
else:
self.assert_obj['请求状态'] = result_obj['status_code']
self.assert_obj['错误信息'] = '规则:' + ruler['rule'] + '值不匹配,期望是:' + str(ruler['value']) + ',返回是:' + str(
matcher[0])
self.assert_result = False
break
elif ruler['type'] == 'length': # 对数组长度进行比较
if len(matcher) == ruler['value']:
continue
else:
self.assert_obj['请求状态'] = result_obj['status_code']
self.assert_obj['错误信息'] = '规则:' + ruler['rule'] + '大小不匹配,期望是:' + str(ruler['value']) + ',返回是:' + str(
len(matcher))
self.assert_result = False
break
else:
self.assert_obj['请求状态'] = result_obj['status_code']
self.assert_obj['错误信息'] = '规则:' + ruler['rule'] + '错误'
self.assert_result = False
break
def setting_param(self):
actives = json.loads(self.test_case['active'])
buiess_content = json.loads(self.result_obj['response_content'])
for active in actives:
try:
p_name = active['p_name']
p_value = jsonpath.jsonpath(buiess_content, active['p_value'])
if isinstance(p_value, bool):
p_value = ''
elif len(p_value) == 0:
p_value = ''
else:
p_value = p_value[0]
config.PARAMS[p_name] = p_value
except Exception as e:
traceback.print_exc()
info = traceback.format_exc()
print(info)
def read_excel():
wb = xlrd.open_workbook(config.TEST_CASE_PATH)
sh = wb.sheet_by_name('Sheet1')
#处理 Excel的数据
for row_ndex in range(sh.nrows):
test_case = dict(zip(sh.row_values(0), sh.row_values(row_ndex)))
try:
if row_ndex == 0:
continue
request_handler(test_case)
if test_case['error_continue'] == 0 and not config.CURRENT_REQUEST_STATUS:
break
except Exception as e:
traceback.print_exc()
info = traceback.format_exc()
print(info)
#config.ALL_REQUEST_RESULT.append({"测试用例": test_case, "请求异常": e})
#发送邮件通知成功与否
message = ''
for result_record in config.ALL_REQUEST_RESULT:
message += json.dumps(result_record,ensure_ascii=False) +'\n\n\n'
send_email(message)
def send_email(message):
if len(message) == 0:
return
my_sender = 'xxx@xxx.com' # 发件人邮箱账号
my_pass = 'xxxxxx' # 发件人邮箱密码
my_user = 'xxx@xxx.com' # 收件人邮箱账号,我这边发送给自己
ret = True
try:
msg = MIMEText(message, 'plain', 'utf-8')
msg['From'] = formataddr(["小课接口测试报告", my_sender]) # 括号里的对应发件人邮箱昵称、发件人邮箱账号
msg['To'] = formataddr(["250", my_user]) # 括号里的对应收件人邮箱昵称、收件人邮箱账号
msg['Subject'] = "小课接口测试报告" # 邮件的主题,也可以说是标题
server = smtplib.SMTP_SSL("imap.exmail.qq.com", 465) # 发件人邮箱中的SMTP服务器,端口是25
server.login(my_sender, my_pass) # 括号中对应的是发件人邮箱账号、邮箱密码
server.sendmail(my_sender, [my_user, ], msg.as_string()) # 括号中对应的是发件人邮箱账号、收件人邮箱账号、发送邮件
server.quit() # 关闭连接
except Exception as e: # 如果 try 中的语句没有执行,则会执行下面的 ret=False
traceback.print_exc()
info = traceback.format_exc()
print(info)
ret = False
if ret:
print("发送邮件成功")
else:
print('发送邮件失败')
#config = Config(r'php域名', r'java域名',r'excel路径.xlsx')
if __name__ == '__main__':
argv = sys.argv
if len(sys.argv) != 4:
print('参数个数不正确')
php_api = argv[1]
java_api = argv[2]
excel_path = argv[3]
config = Config(php_api, java_api, excel_path)
read_excel()
EXCEL模板案列
case_name | header | type | url | method | param_data | body_data | execp_res | memo | active | error_continue |
获取用户token | {} | java | student-service/get-student-token | GET | {"student_id":25009} | {} | [{"rule":"$.code","type":"value","value":0}] | [{"p_name":"$P_student_id","p_type":"int", "p_value":"$.data.student_id"},{"p_name":"$P_token","p_type":"string", "p_value":"$.data.token"}] |
1 | |
获取商品信息 | {"token":"$P_token"} | php | product/info | POST | {} | {"product_id":3100052} | [{"rule":"$.code","type":"value","value":0}] | [{"p_name":"$P_product_id", "p_value":"$.data.id"},{"p_name":"$P_course_id", "p_value":"$.data.course_id"},{"p_name":"$P_total_money", "p_value":"$.data.total_money"},{"p_name":"$P_actual_money", "p_value":"$.data.actual_money"}] |
0 | |
下单 | {"token":"$P_token"} | php | order/create | POST | {} | {"product_id":"$P_product_id","student_id":"$P_student_id", "course_id":"$P_course_id", "total_money":"$P_total_money", "actual_money":"$P_actual_money","group_buy_order_id":null} |
[{"rule":"$.code","type":"value","value":0}] | [{"p_name":"$P_order_id", "p_value":"$.data.order_id"}] |
1 |
字段说明:
case_name:测试用例名称
type:接口提供者(java/php)
url:请求地址path
method:请求接口类型(POST/GET/PUT/DELETE)
param_data:请求参数(需要放在地址栏上的参数),格式json
body_data:请求接口类型不是GET时,请求的body参数
expect_res:对接口返回数据的断言 ;rule:用jsonpath提取返回字段,type:value/length 标识判断字段值的长度还是值匹配,value:如果type == value,则判断返回字段值和value值是否相同;type==length 判断返回的数组或字符串长度是否相同
[{"rule":"$.code","type":"value","value":0},{"rule":"$.code","type":"length","value":1}]
active:将返回的数据值保存,作为下一步骤的请求参数,如下含义案例的含义是:存储返回值的json中的data下的student_id的值,使用时用 $P_student_id参数使用
[{"p_name":"$P_student_id","p_type":"int","p_value":"$.data.student_id"}]
error_continue:当前步骤诊断错误时,是否继续执行后面步骤
案列说明:
上述excel流程是,获取用户token ---》请求商品参数 ----》从商品中获取参数 ----》创建订单 流程,当然还有取消,支付,退款就不一一演示了
来源:oschina
链接:https://my.oschina.net/u/4376386/blog/3605855