简介
此框架是基于Python+Pytest+Requests+Allure+Yaml+Json实现全链路接口自动化测试。
主要流程:解析接口数据包 ->生成接口基础配置(yml) ->生成测试用例(yaml+json) ->生成测试脚本(.py) ->运行测试(pytest) ->生成测试报告(allure)
测试流程:初始化请求 ->处理接口基础信息 ->读取前置接口用例 ->发送前置接口 ->处理当前接口数据 ->发送当前接口 ->检查接口返回
接口自动化测试无非分几大块:测试用例设计、测试脚本编写、测试结果校验、测试报告生成、测试配置管理。
其中常见有几大难点:接口之间依赖关联、测试数据与脚本分离、测试数据参数化处理、全量自动化耗时。
而这些本框架通通已为你解决,你无须编写任何代码,只需要你抓取接口数据包即可。
关于接口依赖:你只要填写前置接口相对路径即可,如果存在数据依赖关系,此时你也仅需要填写前置接口对应的参数值,本框架将自动为你调用和替换关联数据。
关于测试数据:本框架采用yaml记录接口基本信息,当请求参数和结果较大时,将单独保存到json文件中,解决各类数据的错综复杂问题。
关于参数化:本框架采用常用工具使用的变量标识 ${var} ,通过正则表达式,自动检索变量,自动为你替换变量,并且为你提供多种函数助手【$RandInt()、$GenGuid()】为你解决测试数据生成问题。
关于用例执行:本框架利用pytest扩展库,支持多线程模式、失败用例重试、用例模糊匹配等。
目前主要支持四种运行模式:
> 0 -不开启自动生成测试用例功能,将直接运行测试
> 1 -根据手工编写用例,自动生成测试脚本,然后运行测试
> 2 -根据接口抓包数据,自动生成测试用例和测试脚本,然后运行测试
> 3 -根据接口抓包数据,自动生成测试用例和测试脚本,但不运行测试
注意:目前解析仅支持(.chlsj)格式,请使用Charles工具抓包导出JSON Session File
目前支持多种函数助手(以下仅为示例,之后将单独说明):
print('替换变量并计算表达式:', replace('$Eval(${unitCode}*1000+1)', {'unitCode': 9876543210}))
print('生成1-9之间的随机数:', replace('$RandInt(1,9)'))
print('生成10位随机字符:', replace('$RandStr(10)'))
print('从列表中随机选择:', replace('$RandChoice(a,b,c,d)'))
print('生成一个伪手机号:', replace('$GenPhone()'))
print('生成一个guid:', replace('$GenGuid()'))
print('生成一个伪微信ID:', replace('$GenWxid()'))
print('生成一个伪身份证:', replace('$GenNoid()'))
print('生成一个18岁伪身份证:', replace("$GenNoid(y-18)"))
print('生成下个月今天的日期:', replace("$GenDate(m+1)"))
print('生成昨天此时的时间:', replace("$GenDatetime(d-1)"))
替换变量并计算表达式: 9876543210
生成1-9之间的随机数: 9
生成10位随机字符: CB8512d4E6
从列表中随机选择: d
生成一个伪手机号: 18890688629
生成一个guid: 78A6698C-6793-11EB-8221-005056C00008
生成一个伪微信ID: AUTO9K6MRzVGfsNB4ZkIuSdXravD
生成一个伪身份证: 999577202102052043
生成一个18岁伪身份证: 953700200302056259
生成下个月今天的日期: 2021-03-05
生成昨天此时的时间: 2021-02-04 17:21:04.696745
框架流程图
项目结构
启动服务(startup.py)
# -*- coding:utf-8 -*- # @Time : 2021/2/1 # @Author : Leo Zhang # @File : startup.py # *********************** import os import sys import pytest import logging if __name__ == '__main__': from comm.script import writeLogs, writeCase from config import * # 开启日志记录(默认logs目录) writeLogs.MyLogs(ROOT_DIR+'logs') # 判断运行模式 if RC['auto_switch'] == 3: logging.info("根据接口抓包数据,自动生成测试用例和测试脚本,但不运行测试!") writeCase.write_case(DATA_DIR, auto_yaml=True) sys.exit(0) elif RC['auto_switch'] == 2: logging.info("根据接口抓包数据,自动生成测试用例和测试脚本,然后运行测试!") writeCase.write_case(DATA_DIR, auto_yaml=True) elif RC['auto_switch'] == 1: # 如果扫描路径为空在则取项目page目录 if not os.path.exists(RC['scan_dir']): RC['scan_dir'] = PAGE_DIR logging.info("根据手工编写用例,自动生成测试脚本,然后运行测试!") writeCase.write_case(RC['scan_dir'], auto_yaml=False) else: logging.info("不开启自动生成测试用例功能,将直接运行测试!") # 定义运行参数 args_list = ['-vs', TEST_DIR, '-n', str(RC['process']), '--reruns', str(RC['reruns']), '--maxfail', str(RC['maxfail']), '--alluredir', REPORT_DIR+'/xml', '--clean-alluredir'] # 判断是否开启用例匹配 if RC['pattern']: args_list += ['-k ' + str(RC['pattern'])] test_result = pytest.main(args_list) # 生成allure报告 cmd = 'allure generate --clean %s -o %s ' % (REPORT_DIR+'/xml', REPORT_DIR+'/html') os.system(cmd)
运行配置说明(runConfig.yml)
# 运行项目名 project_name: PyDemo # 运行模式: auto_switch: 2 # 0 -不开启自动生成测试用例功能,将直接运行测试 # 1 -根据手工编写用例,自动生成测试脚本,然后运行测试 # 2 -根据接口抓包数据,自动生成测试用例和测试脚本,然后运行测试 # 3 -根据接口抓包数据,自动生成测试用例和测试脚本,但不运行测试 # 注意:目前解析仅支持(.chlsj)格式,请使用Charles工具抓包导出JSON Session File # 扫描测试用例目录(且仅当auto_switch=1时有用) scan_dir: # 使用模糊匹配测试用例(空则匹配所有) pattern: # 执行并发线程数(0表示不开启) process: 0 # 失败重试次数(0表示不重试) reruns: 0 # 本轮测试最大允许失败数(超出则立即结束测试) maxfail: 20 # 接口调用间隔时间(s) interval: 1 # 测试结果校验方式说明(共5种方式): # no_check:不做任何校验 # check_code:仅校验接口返回码code # check_json:校验接口返回码code,并进行json格式比较返回结果(默认方式) # entirely_check:校验接口返回码code,并进行完整比较返回结果 # regular_check:校验接口返回码code,并进行正则匹配返回结果
测试脚本基础模板(test_template.py)
# -*- coding:utf-8 -*- # @Time : 2021/2/2 # @Author : Leo Zhang # @File : test_template.py # **************************** import os import allure import pytest from comm.utils.readYaml import read_yaml_data from comm.unit.initializePremise import init_premise from comm.unit.apiSend import send_request from comm.unit.checkResult import check_result case_yaml = os.path.realpath(__file__).replace('testcase', 'page').replace('py', 'yaml') case_path = os.path.dirname(case_yaml) case_dict = read_yaml_data(case_yaml) @allure.feature(case_dict["test_info"]["title"]) class TestTemplate: @pytest.mark.parametrize("case_data", case_dict["test_case"]) @allure.story("test_template") def test_template(self, case_data): # 初始化请求:执行前置接口+替换关联变量 test_info, case_data = init_premise(case_dict["test_info"], case_data, case_path) # 发送当前接口 code, data = send_request(test_info, case_data) # 校验接口返回 check_result(case_data, code, data)
接口配置示例(apiConfig.yml)
PyDemo: # 首次会根据接口数据包生成,可自行更改或添加新配置,所有配置将作为公共关联值 host: 10.88.88.141:20037 headers: Content-Type: application/x-www-form-urlencoded;charset=UTF-8 cookies: headtoken: xu5YwIZFkVGczMn0H0rot2ps7zRIbvrTHNwMXx1sJXg=
测试用例说明(test.yaml)- 3.12更新
# 用例基本信息 test_info: # 测试用例标题,默认截取请求地址倒数第2个字段名,在报告中作为一级目录显示 title: register # 请求域名,默认读取公共关联值,可修改 host: ${host} # 请求协议 scheme: http # 请求类型 method: POST # 请求地址 address: /api/register/findParam # 参数媒体类型 mime_type: application/x-www-form-urlencoded # 请求头,默认读取公共关联值,可修改 headers: ${headers} # 超时时长(s) timeout: 10 # 是否需要上传文件 file: false # 是否需要获取cookie cookies: false # 是否存在前置接口,如果存在,则填写前置接口用例相对路径,如:/register/test_getAdultCurbactList.yaml premise: false # 测试用例,默认仅生成一个,可手动添加多个 test_case: # 用例概要,默认截取请求地址倒数第1个字段名 - summary: findParam # 用例描述,在报告中作为二级目录显示 describe: test_findParam # 接口请求参数,当总字符数超过200,将转为json文件单独存储 parameter: params: unitCode: '3202112002' first: 0 pym: '' pageSize: 10 page: 0 headtoken: ${headtoken} # 接口检查结果 check_body: # 检查类型,目前支持5种,可自行修改,默认check_json,即仅检查实际与期望结果格式是否一致 check_type: check_json # 期望接口返回码 expected_code: 200 # 期望接口返回消息体,当总字符数超过200,将转为json文件单独存储 expected_result: success: true code: msg: 返回成功 data: - '1' - '1' callTime:
测试报告示例(allure)
核心代码(3.12新增)
script.1、接口数据包解析:依据接口数据,截取对应字段,生成测试用例yaml以及参数文件json。
# -*- coding:utf-8 -*- # @Time : 2020/10/15 # @Author : Leo Zhang # @File : writeCaseYaml.py # **************************** import os import json import logging import urllib.parse from comm.utils.readYaml import write_yaml_file, read_yaml_data from comm.utils.readJson import write_json_file from config import API_CONFIG, PROJECT_NAME def write_case_yaml(har_path): """循环读取接口数据文件 :param har_path: Charles导出文件路径 :return: """ case_file_list = list() logging.info("读取抓包文件主目录: {}".format(har_path)) har_list = os.listdir(har_path) for each in har_list: ext_name = os.path.splitext(each)[1] if ext_name == '.chlsj': logging.info("读取抓包文件: {}".format(each)) file_path = har_path+'/'+each with open(file_path, 'r', encoding='utf-8') as f: har_cts = json.loads(f.read()) har_ct = har_cts[0] # 获取接口基本信息 host = har_ct["host"] port = har_ct["port"] method = har_ct["method"] path = har_ct["path"] headers = har_ct["request"]["header"]['headers'] title = path.split("/")[-1].replace('-', '') module = path.split("/")[-2].replace('-', '') # 创建模块目录 module_path = har_path.split('data')[0] + '/page/' + module try: os.makedirs(module_path) except: pass # 定义api通过配置 api_config = dict() simp_header = dict() for header in headers: # 去除基础请求头 base_header = ['Host', 'Content-Length', 'User-Agent', 'Origin', 'Referer', 'Connection', 'Accept', 'Accept-Encoding', 'Accept-Language'] if header['name'] not in base_header: simp_header[header['name']] = header['value'] api_config['host'] = host+':'+str(port) # 判断是否存在自定义消息头 if simp_header: api_config['headers'] = simp_header else: api_config['headers'] = None api_config['cookies'] = None # 检查是否已存在项目配置信息,没有则写入 rconfig = read_yaml_data(API_CONFIG) if rconfig: if PROJECT_NAME not in rconfig: rconfig[PROJECT_NAME] = api_config write_yaml_file(API_CONFIG, rconfig) else: nconfig = dict() nconfig[PROJECT_NAME] = api_config write_yaml_file(API_CONFIG, nconfig) # 定义测试信息 test_info = dict() test_info["title"] = module test_info["host"] = '${host}' test_info["scheme"] = har_ct["scheme"] test_info["method"] = method test_info["address"] = path test_info["mime_type"] = har_ct["request"]["mimeType"] test_info["headers"] = '${headers}' test_info["timeout"] = 10 test_info["file"] = False test_info["cookies"] = False test_info["premise"] = False # 解析请求报文 parameter = dict() try: if method in 'POST': parameter_list = urllib.parse.unquote(har_ct["request"]["body"]["text"]) elif method in 'PUT': parameter_list = har_ct["request"]["body"]["text"] elif method in 'DELETE': parameter_list = urllib.parse.unquote(har_ct["request"]["body"]["text"]) else: parameter_list = har_ct["query"] if "&" in parameter_list: for key in parameter_list.split("&"): val = key.split("=") parameter[val[0]] = val[1] else: parameter = json.loads(parameter_list) except Exception as e: logging.error("未找到parameter: %s" % e) raise e # 定义用例信息 test_case_list = list() test_case = dict() test_case["summary"] = title test_case["describe"] = 'test_'+title # 定义请求入参信息,且当参数字符总长度大于200时单独写入json文件 if len(str(parameter)) > 200: param_name = title+'_request.json' if param_name not in os.listdir(module_path): # 定义请求json param_dict = dict() param_dict["summary"] = title param_dict["body"] = parameter param_file = module_path+'/'+param_name logging.info("生成请求文件: {}".format(param_file)) write_json_file(param_file, [param_dict]) test_case["parameter"] = param_name else: test_case["parameter"] = parameter # 定义请求返回信息 response_code = har_ct["response"]["status"] response_body = har_ct["response"]["body"]["text"] check = dict() check["check_type"] = 'check_json' check["expected_code"] = response_code expected_request = json.loads(response_body) # 当返回参数字符总长度大于200时单独写入json文件 if len(str(expected_request)) > 200: result_name = title+'_response.json' if result_name not in os.listdir(module_path): # 定义响应json result_dict = dict() result_dict["summary"] = title result_dict["body"] = expected_request result_file = module_path + '/' + result_name logging.info("生成响应文件: {}".format(result_file)) write_json_file(result_file, [result_dict]) check["expected_result"] = result_name else: check["expected_result"] = expected_request test_case["check"] = check test_case_list.append(test_case) # 合并测试信息、用例信息 case_list = dict() case_list["test_info"] = test_info case_list["test_case"] = test_case_list # 写入测试用例(存在则忽略) case_name = 'test_'+title+'.yaml' case_file = module_path+'/'+case_name if not os.path.exists(case_file): logging.info("生成用例文件: {}".format(case_file)) write_yaml_file(case_file, case_list) case_file_list.append(case_file) return case_file_list if __name__ == '__main__': real_path = os.path.dirname(os.path.realpath(__file__)).replace('\\', '/') print('测试用例列表: ', write_case_yaml(real_path+'/data'))
script.2、测试脚本生成:依据测试用例,复制模板并修改相关字段。
# -*- coding:utf-8 -*- # @Time : 2020/10/15 # @Author : Leo Zhang # @File : writeCase.py # ************************ import os from config import ROOT_DIR from comm.script.writeCaseYml import write_case_yaml, read_yaml_data temp_file = ROOT_DIR+'config/test_template.py' def write_case(case_path, auto_yaml=True): """ :param case_path: 用例路径,当auto_yaml为True时,需要传入data目录,否则传入扫描目录 :param auto_yaml: 是否自动生成yaml文件 :return: """ # 判断是否自动生成yaml用例 if auto_yaml: yaml_list = write_case_yaml(case_path) else: yaml_list = list() file_list = os.listdir(case_path) for file in file_list: if '.yaml' in file: yaml_path = case_path+'/'+file yaml_list.append(yaml_path) # 遍历测试用例列表 for yaml_file in yaml_list: test_data = read_yaml_data(yaml_file) test_script = yaml_file.replace('page', 'testcase').replace('.yaml', '.py') # case_name = os.path.basename(test_script).replace('.py', '') case_path = os.path.dirname(test_script) # 判断文件路径是否存在 if not os.path.exists(case_path): os.makedirs(case_path) # 替换模板内容 file_data = '' with open(temp_file, "r", encoding="utf-8") as f: for line in f: if 'TestTemplate' in line: title = test_data['test_info']['title'] line = line.replace('Template', title.title()) if 'test_template' in line: if '@allure.story' in line: describe = test_data['test_case'][0]['describe'] line = line.replace('test_template', describe) else: summary = test_data['test_case'][0]['summary'] line = line.replace('template', summary) file_data += line # 写入新脚本 with open(test_script, "w", encoding="utf-8") as f: f.write(file_data) if __name__ == '__main__': real_path = os.path.dirname(os.path.realpath(__file__)).replace('\\', '/') write_case(real_path + '/data', auto_yaml=True) # write_case(real_path+'/page/oauth', auto_yaml=False)
script.3、运行日志收集:初始化日志模块,保存运行日志。
# -*- coding:utf-8 -*- # @Time : 2020/9/22 # @Author : Leo Zhang # @File : writeLogs.py # ************************ import logging import time import sys import os class MyLogs: def __init__(self, log_path): # 定义日志默认路径和日志名称 if not os.path.exists(log_path): os.makedirs(log_path) runtime = time.strftime('%Y-%m-%d', time.localtime(time.time())) logfile = os.path.join(log_path, runtime+'.log') logfile_err = os.path.join(log_path, runtime+'_error.log') # 第一步,初始化日志对象并设置日志等级 logger = logging.getLogger() logger.setLevel(logging.DEBUG) logger.handlers = [] # 第二步,创建一个handler,用于写入debug日志文件 fh = logging.FileHandler(logfile, mode='a+') fh.setLevel(logging.DEBUG) # 第三步,创建一个handler,用于写入error日志文件 fh_err = logging.FileHandler(logfile_err, mode='a+') fh_err.setLevel(logging.ERROR) # 第四步,再创建一个handler,用于输出info日志到控制台 sh = logging.StreamHandler(sys.stdout) sh.setLevel(logging.INFO) # 第五步,定义handler的输出格式 formatter = logging.Formatter("%(asctime)s - %(filename)s - %(levelname)s: %(message)s") fh.setFormatter(formatter) fh_err.setFormatter(formatter) sh.setFormatter(formatter) # 第六步,将logger添加到handler里面 logger.addHandler(fh) logger.addHandler(fh_err) logger.addHandler(sh)
unit.1、请求协议方法封装:包括post、get、put、delete,以及cookie保存。
# -*- coding:utf-8 -*- # @Time : 2021/2/2 # @Author : Leo Zhang # @File : apiMethod.py # ************************* import os import json import random import logging import requests import simplejson from requests_toolbelt import MultipartEncoder from comm.utils.readYaml import write_yaml_file, read_yaml_data from config import API_CONFIG, PROJECT_NAME def post(headers, address, mime_type, timeout=10, data=None, files=None, cookies=None): """ post请求 :param headers: 请求头 :param address: 请求地址 :param mime_type: 请求参数格式(form_data,raw) :param timeout: 超时时间 :param data: 请求参数 :param files: 文件路径 :param cookies: :return: """ # 判断请求参数类型 if 'form_data' in mime_type: for i in files: value = files[i] if '/' in value: file_parm = i files[file_parm] = (os.path.basename(value), open(value, 'rb')) enc = MultipartEncoder( fields=files, boundary='--------------' + str(random.randint(1e28, 1e29-1)) ) headers['Content-Type'] = enc.content_type response = requests.post(url=address, data=enc, headers=headers, timeout=timeout, cookies=cookies) elif 'data' in mime_type: response = requests.post(url=address, data=data, headers=headers, timeout=timeout, files=files, cookies=cookies) else: response = requests.post(url=address, json=data, headers=headers, timeout=timeout, files=files, cookies=cookies) try: if response.status_code != 200: return response.status_code, response.text else: return response.status_code, response.json() except json.decoder.JSONDecodeError: return response.status_code, None except simplejson.errors.JSONDecodeError: return response.status_code, None except Exception as e: logging.exception('ERROR') logging.error(e) raise def get(headers, address, data, timeout=8, cookies=None): """ get请求 :param headers: 请求头 :param address: 请求地址 :param data: 请求参数 :param timeout: 超时时间 :param cookies: :return: """ response = requests.get(url=address, params=data, headers=headers, timeout=timeout, cookies=cookies) if response.status_code == 301: response = requests.get(url=response.headers["location"]) try: return response.status_code, response.json() except json.decoder.JSONDecodeError: return response.status_code, None except simplejson.errors.JSONDecodeError: return response.status_code, None except Exception as e: logging.exception('ERROR') logging.error(e) raise def put(headers, address, mime_type, timeout=8, data=None, files=None, cookies=None): """ put请求 :param headers: 请求头 :param address: 请求地址 :param mime_type: 请求参数格式(form_data,raw) :param timeout: 超时时间 :param data: 请求参数 :param files: 文件路径 :param cookies: :return: """ if mime_type == 'raw': data = json.dumps(data) elif mime_type == 'application/json': data = json.dumps(data) response = requests.put(url=address, data=data, headers=headers, timeout=timeout, files=files, cookies=cookies) try: return response.status_code, response.json() except json.decoder.JSONDecodeError: return response.status_code, None except simplejson.errors.JSONDecodeError: return response.status_code, None except Exception as e: logging.exception('ERROR') logging.error(e) raise def delete(headers, address, data, timeout=8, cookies=None): """ delete请求 :param headers: 请求头 :param address: 请求地址 :param data: 请求参数 :param timeout: 超时时间 :param cookies: :return: """ response = requests.delete(url=address, params=data, headers=headers, timeout=timeout, cookies=cookies) try: return response.status_code, response.json() except json.decoder.JSONDecodeError: return response.status_code, None except simplejson.errors.JSONDecodeError: return response.status_code, None except Exception as e: logging.exception('ERROR') logging.error(e) raise def save_cookie(headers, address, mime_type, timeout=8, data=None, files=None, cookies=None): """ 保存cookie信息 :param headers: 请求头 :param address: 请求地址 :param mime_type: 请求参数格式(form_data,raw) :param timeout: 超时时间 :param data: 请求参数 :param files: 文件路径 :param cookies: :return: """ if 'data' in mime_type: response = requests.post(url=address, data=data, headers=headers, timeout=timeout, files=files, cookies=cookies) else: response = requests.post(url=address, json=data, headers=headers, timeout=timeout, files=files, cookies=cookies) try: cookies = response.cookies.get_dict() # 读取api配置并写入最新的cookie结果 aconfig = read_yaml_data(API_CONFIG) aconfig[PROJECT_NAME]['cookies'] = cookies write_yaml_file(API_CONFIG, aconfig) logging.debug("cookies已保存,结果为:{}".format(cookies)) except json.decoder.JSONDecodeError: return response.status_code, None except simplejson.errors.JSONDecodeError: return response.status_code, None except Exception as e: logging.exception('ERROR') logging.error(e) raise
unit.2、接口发送方法封装:读取测试用例,拼接请求信息,发送请求,返回结果。
# -*- coding:utf-8 -*- # @Time : 2021/2/2 # @Author : Leo Zhang # @File : apiSend.py # *********************** import logging import allure import time from config import INTERVAL from comm.unit import apiMethod def send_request(test_info, case_data): """ 封装请求 :param test_info: 测试信息 :param case_data: 用例数据 :return: """ try: # 获取用例基本信息 host = test_info["host"] scheme = test_info["scheme"] method = test_info["method"].upper() address = test_info["address"] mime_type = test_info["mime_type"] headers = test_info["headers"] cookies = test_info["cookies"] file = test_info["file"] timeout = test_info["timeout"] summary = case_data["summary"] parameter = case_data["parameter"] except Exception as e: raise KeyError('获取用例基本信息失败:{}'.format(e)) request_url = scheme + "://" + host + address logging.info("=" * 150) logging.info("请求接口:%s" % str(summary)) logging.info("请求地址:%s" % request_url) logging.info("请求头: %s" % str(headers)) logging.info("请求参数: %s" % str(parameter)) # 判断是否保存cookies if summary == 'save_cookies': with allure.step("保存cookies信息"): allure.attach(name="请求接口", body=str(summary)) allure.attach(name="请求地址", body=request_url) allure.attach(name="请求头", body=str(headers)) allure.attach(name="请求参数", body=str(parameter)) apiMethod.save_cookie(headers=headers, address=request_url, mime_type=mime_type, data=parameter, cookies=cookies, timeout=timeout) # 判断接口请求类型 if method == 'POST': logging.info("请求方法: POST") # 判断是否上传文件 if file: with allure.step("POST上传文件"): allure.attach(name="请求接口", body=str(summary)) allure.attach(name="请求地址", body=request_url) allure.attach(name="请求头", body=str(headers)) allure.attach(name="请求参数", body=str(parameter)) result = apiMethod.post(headers=headers, address=request_url, mime_type=mime_type, files=parameter, cookies=cookies, timeout=timeout) else: with allure.step("POST请求接口"): allure.attach(name="请求接口", body=str(summary)) allure.attach(name="请求地址", body=request_url) allure.attach(name="请求头", body=str(headers)) allure.attach(name="请求参数", body=str(parameter)) result = apiMethod.post(headers=headers, address=request_url, mime_type=mime_type, data=parameter, cookies=cookies, timeout=timeout) elif method == 'GET': logging.info("请求方法: GET") with allure.step("GET请求接口"): allure.attach(name="请求接口", body=str(summary)) allure.attach(name="请求地址", body=request_url) allure.attach(name="请求头", body=str(headers)) allure.attach(name="请求参数", body=str(parameter)) result = apiMethod.get(headers=headers, address=request_url, data=parameter, cookies=cookies, timeout=timeout) elif method == 'PUT': logging.info("请求方法: PUT") # 判断是否上传文件 if file: with allure.step("PUT上传文件"): allure.attach(name="请求接口", body=str(summary)) allure.attach(name="请求地址", body=request_url) allure.attach(name="请求头", body=str(headers)) allure.attach(name="请求参数", body=str(parameter)) result = apiMethod.put(headers=headers, address=request_url, mime_type=mime_type, files=parameter, cookies=cookies, timeout=timeout) else: with allure.step("PUT请求接口"): allure.attach(name="请求接口", body=str(summary)) allure.attach(name="请求地址", body=request_url) allure.attach(name="请求头", body=str(headers)) allure.attach(name="请求参数", body=str(parameter)) result = apiMethod.put(headers=headers, address=request_url, mime_type=mime_type, data=parameter, cookies=cookies, timeout=timeout) elif method == 'DELETE': logging.info("请求方法: DELETE") with allure.step("DELETE请求接口"): allure.attach(name="请求接口", body=str(summary)) allure.attach(name="请求地址", body=request_url) allure.attach(name="请求头", body=str(headers)) allure.attach(name="请求参数", body=str(parameter)) result = apiMethod.delete(headers=headers, address=request_url, data=parameter, cookies=cookies, timeout=timeout) else: result = {"code": None, "data": None} logging.info("请求接口结果:\n %s" % str(result)) time.sleep(INTERVAL) return result
unit.3、接口初始化处理:读取公共关联配置,获取当前关联值,执行前置接口,替换关联值,返回用例信息。
# -*- coding:utf-8 -*- # @Time : 2021/2/3 # @Author : Leo Zhang # @File : initializePremise.py # ************************** import logging import time import json from json import JSONDecodeError from config import PAGE_DIR, PROJECT_NAME, API_CONFIG from comm.unit import apiSend, readRelevance, replaceRelevance from comm.utils import readYaml def read_json(summary, json_obj, case_path): """ 校验内容读取 :param summary: 用例名称 :param json_obj: json文件或数据对象 :param case_path: case路径 :return: """ if isinstance(json_obj, dict): return json_obj else: try: # 读取json文件指定用例数据 with open(case_path+'/'+json_obj, "r", encoding="utf-8") as js: data_list = json.load(js) for data in data_list: if data['summary'] == summary: return data['body'] except FileNotFoundError: raise Exception("用例关联文件不存在\n文件路径: %s" % json_obj) except JSONDecodeError: raise Exception("用例关联的文件有误\n文件路径: %s" % json_obj) def init_premise(test_info, case_data, case_path): """用例前提条件执行,提取关键值 :param test_info: 测试信息 :param case_data: 用例数据 :param case_path: 用例路径 :return: """ # 获取项目公共关联值 aconfig = readYaml.read_yaml_data(API_CONFIG) __relevance = aconfig[PROJECT_NAME] # 处理测试信息 test_info = replaceRelevance.replace(test_info, __relevance) logging.debug("测试信息处理结果:{}".format(test_info)) # 处理Cookies if test_info['cookies']: cookies = aconfig[PROJECT_NAME]['cookies'] logging.debug("请求Cookies处理结果:{}".format(cookies)) # 判断是否存在前置接口 pre_case_path = test_info["premise"] if pre_case_path: # 获取前置接口用例 logging.info("获取前置接口测试用例:{}".format(pre_case_path)) pre_case_path = PAGE_DIR + pre_case_path pre_case_dict = readYaml.read_yaml_data(pre_case_path) pre_test_info = pre_case_dict['test_info'] pre_case_data = pre_case_dict['test_case'][0] # 判断前置接口是否也存在前置接口 if pre_test_info["premise"]: init_premise(pre_test_info, pre_case_data, pre_case_path) for i in range(3): # 处理前置接口测试信息 pre_test_info = replaceRelevance.replace(pre_test_info, __relevance) logging.debug("测试信息处理结果:{}".format(pre_test_info)) # 处理前置接口Cookies if pre_test_info['cookies']: cookies = aconfig[PROJECT_NAME]['cookies'] logging.debug("请求Cookies处理结果:{}".format(cookies)) # 处理前置接口入参:获取入参-替换关联值-发送请求 pre_parameter = read_json(pre_case_data['summary'], pre_case_data['parameter'], pre_case_path) pre_parameter = replaceRelevance.replace(pre_parameter, __relevance) pre_case_data['parameter'] = pre_parameter logging.debug("请求参数处理结果:{}".format(pre_parameter)) logging.info("执行前置接口测试用例:{}".format(pre_test_info)) code, data = apiSend.send_request(pre_test_info, pre_case_data) # 检查接口是否调用成功 if data: # 处理当前接口入参:获取入参-获取关联值-替换关联值 parameter = read_json(case_data['summary'], case_data['parameter'], case_path) __relevance = readRelevance.get_relevance(data, parameter, __relevance) parameter = replaceRelevance.replace(parameter, __relevance) case_data['parameter'] = parameter logging.debug("请求参数处理结果:{}".format(parameter)) # 获取当前接口期望结果:获取期望结果-获取关联值-替换关联值 expected_rs = read_json(case_data['summary'], case_data['check']['expected_result'], case_path) parameter['data'] = data __relevance = readRelevance.get_relevance(parameter, expected_rs, __relevance) expected_rs = replaceRelevance.replace(expected_rs, __relevance) case_data['check']['expected_result'] = expected_rs logging.debug("期望返回处理结果:{}".format(case_data)) break else: time.sleep(1) logging.error("前置接口请求失败!等待1秒后重试!") else: logging.info("前置接口请求失败!尝试三次失败!") raise Exception("获取前置接口关联数据失败!") else: # 处理当前接口入参:获取入参-获取关联值-替换关联值 parameter = read_json(case_data['summary'], case_data['parameter'], case_path) parameter = replaceRelevance.replace(parameter, __relevance) case_data['parameter'] = parameter logging.debug("请求参数处理结果:{}".format(parameter)) # 获取当前接口期望结果:获取期望结果-获取关联值-替换关联值 expected_rs = read_json(case_data['summary'], case_data['check']['expected_result'], case_path) __relevance = readRelevance.get_relevance(parameter, expected_rs, __relevance) expected_rs = replaceRelevance.replace(expected_rs, __relevance) case_data['check']['expected_result'] = expected_rs logging.debug("期望返回处理结果:{}".format(case_data)) return test_info, case_data
unit.4、获取关联值配置:通过正则匹配${}检索关联值,然后获取消息体中的对应值。
# -*- coding:utf-8 -*- # @Time : 2021/1/8 # @Author : Leo Zhang # @File : readRelevance.py # **************************** import logging import re __relevance = "" def get_value(data, value): """获取数据中的值 :param data: :param value: :return: """ global __relevance if isinstance(data, dict): if value in data: __relevance = data[value] else: for key in data: __relevance = get_value(data[key], value) elif isinstance(data, list): for key in data: if isinstance(key, dict): __relevance = get_value(key, value) break return __relevance def get_relevance(data, relevance_list, relevance=None): """获取关联键值对 :param data: :param relevance_list: :param relevance: :return: """ # 获取关联键列表 relevance_list = re.findall(r"\${(.*?)}", str(relevance_list)) relevance_list = list(set(relevance_list)) logging.debug("获取关联键列表:\n%s" % relevance_list) # 判断关联键和源数据是否有值 if (not data) or (not relevance_list): return relevance # 判断是否存在其他关联键对象 if not relevance: relevance = dict() # 遍历关联键 for each in relevance_list: if each in relevance: pass # # 考虑到一个关联键,多个值 # if isinstance(relevance[each], list): # a = relevance[each] # a.append(relevance_value) # relevance[each] = a # else: # a = relevance[each] # b = list() # b.append(a) # b.append(relevance_value) # relevance[each] = b else: # 从结果中提取关联键的值 relevance[each] = get_value(data, each) logging.debug("提取关联键对象:\n%s" % relevance) return relevance
unit.5、替换关联值:首先替换关联值,然后替换函数助手生成数据,最后替换表达式结果。
# -*- coding:utf-8 -*- # @Time : 2020/12/09 # @Author : Leo Zhang # @File : replaceRelevance.py # **************************** import re from comm.utils.randomly import * pattern_var = r"\${(.*?)}" pattern_eval = r"\$Eval\((.*?)\)" pattern_str = r'\$RandStr\(([0-9]*?)\)' pattern_int = r'\$RandInt\(([0-9]*,[0-9]*?)\)' pattern_choice = r"\$RandChoice\((.*?)\)" pattern_float = r'\$RandFloat\(([0-9]*,[0-9]*,[0-9]*)\)' pattern_phone = r'\$GenPhone\(\)' pattern_guid = r'\$GenGuid\(\)' pattern_wxid = r'\$GenWxid\(\)' pattern_noid = r'\$GenNoid\((.*?)\)' pattern_date = r'\$GenDate\((.*?)\)' pattern_datetime = r'\$GenDatetime\((.*?)\)' def replace_pattern(pattern, value): """替换正则表达式 :param pattern: 匹配字符 :param value: 匹配值 :return: """ patterns = pattern.split('(.*?)') return ''.join([patterns[0], value, patterns[-1]]) def replace_relevance(param, relevance=None): """替换变量关联值 :param param: 参数对象 :param relevance: 关联对象 :return: """ result = re.findall(pattern_var, str(param)) if (not result) or (not relevance): pass else: for each in result: try: # 关联参数多值替换 # relevance_index = 0 # if isinstance(relevance[each], list): # try: # param = re.sub(pattern, relevance[each][relevance_index], param, count=1) # relevance_index += 1 # except IndexError: # relevance_index = 0 # param = re.sub(pattern, relevance[each][relevance_index], param, count=1) # relevance_index += 1 value = relevance[each] pattern = re.compile(r'\${' + each + '}') try: param = re.sub(pattern, value, param) except TypeError: param = value except KeyError: raise KeyError('替换变量{0}失败,未发现变量对应关联值!\n关联列表:{1}'.format(param, relevance)) # pass return param def replace_eval(param): """替换eval表达式结果 :param param: 参数对象 :return: """ result = re.findall(pattern_eval, str(param)) if not result: pass else: for each in result: try: if 'import' in each: raise Exception('存在非法标识import') else: value = str(eval(each)) param = re.sub(pattern_eval, value, param) except KeyError as e: raise Exception('获取值[ % ]失败!\n%'.format(param, e)) except SyntaxError: pass return param def replace_random(param): """替换随机方法参数值 :param param: :return: """ int_list = re.findall(pattern_int, str(param)) str_list = re.findall(pattern_str, str(param)) choice_list = re.findall(pattern_choice, str(param)) guid_list = re.findall(pattern_guid, str(param)) noid_list = re.findall(pattern_noid, str(param)) phone_list = re.findall(pattern_phone, str(param)) wxid_list = re.findall(pattern_wxid, str(param)) date_list = re.findall(pattern_date, str(param)) datetime_list = re.findall(pattern_datetime, str(param)) if len(str_list): for each in str_list: # pattern = re.compile(r'\$RandStr\(' + each + r'\)') # param = re.sub(pattern, str(random_str(each)), param, count=1) param = re.sub(pattern_str, str(random_str(each)), param, count=1) if len(int_list): for each in int_list: param = re.sub(pattern_int, str(random_int(each)), param, count=1) if len(choice_list): for each in choice_list: param = re.sub(pattern_choice, str(random_choice(each)), param, count=1) if len(date_list): for each in date_list: param = re.sub(pattern_date, str(generate_date(each)), param, count=1) if len(datetime_list): for each in datetime_list: param = re.sub(pattern_datetime, str(generate_datetime(each)), param, count=1) if len(noid_list): for each in noid_list: param = re.sub(pattern_noid, str(generate_noid(each)), param, count=1) if len(phone_list): for i in phone_list: param = re.sub(pattern_phone, str(generate_phone()), param, count=1) if len(guid_list): for i in guid_list: param = re.sub(pattern_guid, generate_guid(), param, count=1) if len(wxid_list): for i in wxid_list: param = re.sub(pattern_wxid, generate_wxid(), param, count=1) return param def replace(param, relevance=None): """替换参数对应关联数据 :param param: 参数对象 :param relevance: 关联对象 :return: """ if not param: pass elif isinstance(param, dict): for key, value in param.items(): if isinstance(value, dict): param[key] = replace(value, relevance) elif isinstance(value, list): for index, sub_value in enumerate(value): param[key][index] = replace(sub_value, relevance) else: value = replace_relevance(value, relevance) value = replace_random(value) value = replace_eval(value) param[key] = value elif isinstance(param, list): for index, value in enumerate(param): param[index] = replace(value, relevance) else: param = replace_relevance(param, relevance) param = replace_random(param) param = replace_eval(param) return param if __name__ == '__main__': print('替换变量并计算表达式:', replace('$Eval(${unitCode}*1000+1)', {'unitCode': 9876543210})) print('生成1-9之间的随机数:', replace('$RandInt(1,9)')) print('生成10位随机字符:', replace('$RandStr(10)')) print('从列表中随机选择:', replace('$RandChoice(a,b,c,d)')) print('生成一个伪手机号:', replace('$GenPhone()')) print('生成一个guid:', replace('$GenGuid()')) print('生成一个伪微信ID:', replace('$GenWxid()')) print('生成一个伪身份证:', replace('$GenNoid()')) print('生成一个18岁伪身份证:', replace("$GenNoid(y-18)")) print('生成下个月今天的日期:', replace("$GenDate(m+1)")) print('生成昨天此时的时间:', replace("$GenDatetime(d-1)"))
unit.6、接口结果校验:包括不检查、仅检查接口状态码、对比实际与期望结果格式、完全对比校验、正则方式校验。
# -*- coding:utf-8 -*- # @Time : 2021/2/2 # @Author : Leo Zhang # @File : checkResult.py # *************************** import re import allure import operator def check_json(src_data, dst_data): """ 校验的json :param src_data: 检验内容 :param dst_data: 接口返回的数据 :return: """ if isinstance(src_data, dict): for key in src_data: if key not in dst_data: raise Exception("JSON格式校验,关键字 %s 不在返回结果 %s 中!" % (key, dst_data)) else: this_key = key if isinstance(src_data[this_key], dict) and isinstance(dst_data[this_key], dict): check_json(src_data[this_key], dst_data[this_key]) elif not isinstance(src_data[this_key], type(dst_data[this_key])): raise Exception("JSON格式校验,关键字 %s 返回结果 %s 与期望结果 %s 类型不符" % (this_key, src_data[this_key], dst_data[this_key])) else: pass else: raise Exception("JSON校验内容非dict格式:{}".format(src_data)) def check_result(case_data, code, data): """ 校验测试结果 :param case_data: 用例数据 :param code: HTTP状态 :param data: 返回的接口json数据 :return: """ try: # 获取用例检查信息 check_type = case_data['check']['check_type'] expected_code = case_data['check']['expected_code'] expected_result = case_data['check']['expected_result'] except Exception as e: raise KeyError('获取用例检查信息失败:{}'.format(e)) if check_type == 'no_check': with allure.step("不校验结果"): pass elif check_type == 'check_code': with allure.step("HTTP状态码校验"): allure.attach(name="实际code", body=str(code)) allure.attach(name="期望code", body=str(expected_code)) allure.attach(name='实际data', body=str(data)) if int(code) != expected_code: raise Exception("http状态码错误!\n %s != %s" % (code, expected_code)) elif check_type == 'check_json': with allure.step("JSON格式校验结果"): allure.attach(name="实际code", body=str(code)) allure.attach(name="期望code", body=str(expected_code)) allure.attach(name='实际data', body=str(data)) allure.attach(name='期望data', body=str(expected_result)) if int(code) == expected_code: if not data: data = "{}" check_json(expected_result, data) else: raise Exception("http状态码错误!\n %s != %s" % (code, expected_code)) elif check_type == 'entirely_check': with allure.step("完全校验结果"): allure.attach(name="实际code", body=str(code)) allure.attach(name="期望code", body=str(expected_code)) allure.attach(name='实际data', body=str(data)) allure.attach(name='期望data', body=str(expected_result)) if int(code) == expected_code: result = operator.eq(expected_result, data) if not result: raise Exception("完全校验失败! %s ! = %s" % (expected_result, data)) else: raise Exception("http状态码错误!\n %s != %s" % (code, expected_code)) elif check_type == 'regular_check': if int(code) == expected_code: try: result = "" if isinstance(expected_result, list): for i in expected_result: result = re.findall(i.replace("\"", "\""), str(data)) allure.attach('校验完成结果\n', str(result)) else: result = re.findall(expected_result.replace("\"", "\'"), str(data)) with allure.step("正则匹配校验结果"): allure.attach(name="实际code", body=str(code)) allure.attach(name="期望code", body=str(expected_code)) allure.attach(name='实际data', body=str(data)) allure.attach(name='期望data', body=str(expected_result).replace("\'", "\"")) allure.attach(name=expected_result.replace("\"", "\'") + '校验完成结果', body=str(result).replace("\'", "\"")) if not result: raise Exception("正则未校验到内容! %s" % expected_result) except KeyError: raise Exception("正则校验执行失败! %s\n正则表达式为空时" % expected_result) else: raise Exception("http状态码错误!\n %s != %s" % (code, expected_code)) else: raise Exception("无该校验方式%s" % check_type)
utils.1、函数助手:调用不同方法来生成相关测试数据,比如生成指定长度随机字符、生成指定日期时间、生成唯一标识guid等。
# -*- coding:utf-8 -*- # @Time : 2020/12/10 # @Author : Leo Zhang # @File : randomly.py # ************************* import string import random import datetime from dateutil.relativedelta import relativedelta def random_str(str_len): """从a-zA-Z0-9生成制定数量的随机字符 :param str_len: 字符串长度 :return: """ try: str_len = int(str_len) except ValueError: raise Exception("调用随机字符失败,[ %s ]长度参数有误!" % str_len) strings = ''.join(random.sample(string.hexdigits, +str_len)) return strings def random_int(scope): """获取随机整型数据 :param scope: 数据范围 :return: """ try: start_num, end_num = scope.split(",") start_num = int(start_num) end_num = int(end_num) except ValueError: raise Exception("调用随机整数失败,[ %s ]范围参数有误!" % str(scope)) if start_num <= end_num: number = random.randint(start_num, end_num) else: number = random.randint(end_num, start_num) return number def random_float(data): """获取随机浮点数据 :param data: 数组 :return: """ try: start_num, end_num, accuracy = data.split(",") start_num = int(start_num) end_num = int(end_num) accuracy = int(accuracy) except ValueError: raise Exception("调用随机浮点数失败,[ %s ]范围参数或精度有误!" % data) if start_num <= end_num: number = random.uniform(start_num, end_num) else: number = random.uniform(end_num, start_num) number = round(number, accuracy) return number def random_choice(data): """获取数组随机值 :param data: 数组 :return: """ _list = data.split(",") each = random.choice(_list) return each def get_date_mark(now, mark, num): if 'y' == mark: return now + relativedelta(years=num) elif 'm' == mark: return now + relativedelta(months=num) elif 'd' == mark: return now + relativedelta(days=num) elif 'h' == mark: return now + relativedelta(hours=num) elif 'M' == mark: return now + relativedelta(minutes=num) elif 's' == mark: return now + relativedelta(seconds=num) else: raise Exception("日期字段标识[ %s ]错误, 请使用[年y,月m,日d,时h,分M,秒s]标识!" % mark) def generate_date(expr=''): """生成日期对象(不含时分秒) :param expr: 日期表达式,如"d-1"代表日期减1 :return: """ today = datetime.date.today() if expr: try: mark = expr[:1] num = int(expr[1:]) except (TypeError, NameError): raise Exception("调用生成日期失败,日期表达式[ %s ]有误!" % expr) return get_date_mark(today, mark, num) else: return today def generate_datetime(expr=''): """生成日期时间对象(含时分秒毫秒) :param expr: 日期表达式,如"d-1"代表日期减1 :return: """ now = datetime.datetime.now() if expr: try: mark = expr[:1] num = int(expr[1:]) except (TypeError, NameError): raise Exception("调用生成日期失败,日期表达式[ %s ]有误!" % expr) return get_date_mark(now, mark, num) else: return now def generate_timestamp(expr=''): """生成时间戳(13位) :param expr: 日期表达式,如"d-1"代表日期减1 :return: """ datetime_obj = generate_datetime(expr) return int(datetime.datetime.timestamp(datetime_obj)) * 1000 def generate_guid(): """基于MAC地址+时间戳+随机数来生成GUID :param: :return: """ import uuid return str(uuid.uuid1()).upper() def generate_wxid(): """基于AUTO标识+26位英文字母大小写+数字生成伪微信ID :param: :return: """ return 'AUTO' + ''.join(random.sample(string.ascii_letters + string.digits, 24)) def generate_noid(expr=''): """基于6位随机数字+出生日期+4位随机数生成伪身份证 :param expr: 日期表达式,如"d-1"代表日期减1 :return: """ birthday = generate_date(expr) birthday = str(birthday).replace('-', '') return int(str(random.randint(100000, 999999)) + birthday + str(random.randint(1000, 9999))) def generate_phone(): """基于三大运营商号段+随机数生成伪手机号 :param: :return: """ ctcc = [133,153,173,177,180,181,189,191,193,199] cucc = [130,131,132,155,156,166,175,176,185,186,166] cmcc = [134,135,136,137,138,139,147,150,151,152,157,158,159,172,178,182,183,184,187,188,198] begin = 10 ** 7 end = 10 ** 8 - 1 prefix = random.choice(ctcc+cucc+cmcc) return str(prefix) + str(random.randint(begin, end)) if __name__ == '__main__': # 简单随机数据 print(random_str(16)) print(random_int("100,200")) print(random_float("200,100,5")) print(random_choice("aaa,bbb,ccc")) # 生成日期数据 print(generate_date()) print(generate_datetime()) print(generate_date('m+1')) print(generate_datetime('d+1')) print(generate_timestamp('s+100')) print(generate_noid('y-18')) # 生成常用数据 print(generate_guid()) print(generate_wxid()) print(generate_noid()) print(generate_phone())
实战演示
1、首先环境准备:Python + Allure (这里不做详细说明,请参考我Pytest分类博文)
接着下载项目:https://github.com/Leozhanggg/ApiTesting (方便的话给个星,不要白嫖呀,哈哈。。。)
然后加载依赖:pip install -r requirements.txt
(或者使用Pycharm打开,会自动弹出提示安装)
2、使用Charles工具抓取接口数据包,并且导出选择JSON Session File (.chlsj) 格式 (工具自己百度下载吧)
3、新建一个项目MyTest目录和一个data目录,把抓取的接口数据包放置进来,然后修改runConfig.yml项目名为MyTest
4、直接开始运行,然后你就会发现项目目录多了很多文件,测试已经完成。。。没错,就是这么简单,你还可以查看allure报告。
谈谈我自己
以往我使用过多种基于Python的自动化测试框架,特别是robotframework,简单易上手,对于培养普通测试工程师比较迅速,但是优点同时也是缺点,由于RF自身局限性,会让简单的语法变得复杂化,
如果你不做分层处理,可能会出现一条简单的测试用例编需要写上百行,后期维护更是非常麻烦,
我记得有一次检查测试工程师的自动化测试用例时,发现竟然有两百多行,对于RF这种表格语法两百多行你知道阅读是多么的痛苦嘛。。。
当然这也是源于我们的项目性质,由于大数据业务,接口只是一小部分,而数据的校验才是大头,
并且涉及到多类数据库,比如redis、mysql、es、hbase、solr等,而且有的接口会同时保存到多个表然后同步到多个数据库,校验点数不过来。。。
就这样前前后后我带领着几个测试工程师改了几版,虽然最后大大的减少了测试代码,但是依然还是很多,
并且运行时长很难解决。所以从去年开始使用pytest测试框架,当然这也是我首次接触pytest,第一个项目也改了几版,但是由于纯pytest编写,
所有的东西都在一块,改起来也比较简单,最终的效果当然是质的提升,首先时代码方面可以轻松的做分层处理,不会受到RF之类的框架限制,
而在执行时长方面可以采用pytest自身的多线程模块,大大减少执行时长,同时大大提高了框架的扩展性。
而本框架源于https://github.com/wangxiaoxi3/API_service项目,加上自己实际项目实施经验重构而来,保留了核心功能,增加了自己对接口自动化测试的理解。
特别对于关联值处理方面,不在需要手动标记,而采用自动检索方式,另外关于前置接口处理,也不在需要手动编写,只需要指定前置接口相对路径即可,并且多个用例可以嵌套。
另外在请求地址和消息头上,不在需要手动配置,将在接口数据解析中自动筛选消息头和请求地址,然后写入接口公共配置中,除非有变动,否则无需做任何配置。
但是目前开发的第一版并没有加入数据库校验,仅为了单纯接口的自动化测试,后期将考虑加入数据库校验模块。以下为本人实际项目数据库校验示例:
※ 如果有任何疑问可以留言,当然如果觉得写得不错可以收藏、推荐一下,另外github帮忙给个星!!!
※ ApiTesting交流群(QQ):149999711
作者:Leozhanggg
出处:https://www.cnblogs.com/leozhanggg/p/14373878.html
源码:https://github.com/Leozhanggg/ApiTesting
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。