该框架是基于OpenAPI接口自动化测试的理论搭建,基于python+requests+pytest实现。通过封装主要核心方法,在一定的基础上增强代码后期的可维护性和易读性。
common:存放一些封装的公共的方法,例如断言、日志、读取测试用例数据的方法
data:存放测试数据
reports:存放生成的测试报告
test_OpenAPI:存放用例以及接口封装
page_api:按照对象对接口进行封装
test_cases:测试用例
utils:存放封装的工具,例如读取文件、发送接口请求等
base_url:测试的接口url地址
conftest:存放获取token的方法
get_token:存放获取token时需要使用的数据
main:运行测试用例的py文件
pytest.ini:pytest的配置文件
import pytest
from common.logger import Logger
from utils.read_yaml import read_yaml
from utils.requests_utils import RequestUtils
@pytest.fixture(scope="session")
def get_headers():
try:
# 读取获取token需要用到的数据
data_token = read_yaml("./get_token.yml")
# 获取token
response = RequestUtils().send_request(url=data_token["url"], method=data_token["method"],
json=data_token["json"])
token_value = response.json()["access_token"]
Logger.logger_in().info('token获取成功,token值为:{}'.format(token_value))
headers = {
"Authorization": "Bearer" + ' ' + token_value,
"Host": "openapi.italent.cn",
"Content-Type": "application/json"
}
return headers
except Exception as e:
# print(repr(e))
Logger.logger_in().error('获取token失败,原因为{}'.format(repr(e)))
我们知道,在pytest中,conftest文件会在所有的测试用例执行之前运行,常常用于每个接口用例需要使用到共同的token、需要共用测试用例的数据、每个接口用例需要共用配置等等。
在本框架中,我将token的获取设计在conftest中:通过读取yaml文件,将获取token所需要的参数拿到;获取token后,将token保存在headers中,并返回,便于我们在进行接口请求时,直接使用含有token的请求头。并将token的获取过程生成日志,便于后期定位问题。
class Assert:
# 实现断言,并且将断言的结果写入到日志中
@staticmethod
def code_assert(response, assert_type,assert_msg):
if assert_type == "code":
try:
assert response.status_code == assert_msg
Logger.logger_in().info('断言成功!')
except Exception as e:
Logger.logger_in().error('断言失败!原因是:{}'.format(repr(e)))
# print(response.text)
else:
print("请输入code")
if assert_type == "massage":
try:
assert response.text == assert_msg
Logger.logger_in().info('断言成功!')
except Exception as e:
Logger.logger_in().error('断言失败!原因是:{}'.format(repr(e)))
# print(response.text)
else:
print("请输入massage")
通过封装Assert类,实现断言的效果,并且将断言的结果记录到日志中。
使用该方法时,需要传入接口响应体、想要断言的具体内容以及预期结果,在本框架中主要考虑的是对响应的状态码以及响应体信息进行断言;想要对状态进行断言则输入“code”,想要对响应体信息进行断言,则输入"massage";若获取到的响应中对应的信息与预期结果相等时,则断言成功。
因为可能存在断言失败的情况,所以使用了python的异常处理机制,当发生异常时,会对异常进行捕获,最后通过 Logger类中的logger_in()方法,将断言结果写入到日志中。
后续优化为:
# -*- coding: gbk -*-
from common.logger import Logger
class Assert:
# 实现断言,并且将断言的结果写入到日志中
@staticmethod
def custom_assert(response_assert,assert_msg):
try:
assert response_assert == assert_msg
Logger.logger_in().info('断言成功!')
except Exception as e:
Logger.logger_in().error('断言失败!原因是:{}'.format(repr(e)))
考虑到仅上述方法的局限性(只能通过code以及massage来做断言),将断言部分进行了优化,传入参与断言的response_assert,与预期结果assert_msg进行比对,更加灵活。
class GetData:
@staticmethod
def get_data(filename):
data_names = re.findall(r'_(.*)\.', filename)
data_name = str(data_names[0]) + '.xlsx'
path = os.path.join('./data/', data_name)
excel_cases = ExcelHandler(path)
data_cases = excel_cases.read_excel("Sheet1")
print(data_cases)
return data_cases
之前的数据读取都是直接写在用例里面的,通过调用 ExcelHandler类中的read_excel方法来读取测试数据,后期考虑到设计的话存在的一个问题:当我们用例数增多时,如果文件路径发生了改变,那么要修改每一个测试用例中的调用路径,这样的成本是巨大的,不利于后期维护。
为了解决上述问题,我封装了一个GetData类。通过filename传入参数,使用正则表达式,截取出filename中_后的关键字,使用os.path.join()方法将路径和文件名拼接在一起,封装一个静态方法,方便在每个用例处调用直接调用,减少不必要的内存占用和性能消耗。但这里也带来一个限制,那就是测试用例的文件名_ 后的文字必须于测试数据的文件名保持一致。
这样封装的话,后期如果文件路径发生了变化,我们只需要修改类中的的路径即可。
class Logger:
__logger = None
@classmethod
def logger_in(cls):
if cls.__logger is None:
# 创建日志器
cls.__logger = logging.getLogger("APIlogger")
cls.__logger.setLevel(logging.DEBUG)
# 判断是否存在handler,不然每次都会新建一个handler,导致日志重复输出
if not cls.__logger.handlers:
# 获取当前日期为文件名,年份最后2位+月份+日期
file_name = str(datetime.datetime.now().strftime('%g' + '%m' + "%d")) + '.log'
# 创建处理器
handler = logging.FileHandler(os.path.join('./log', file_name))
# handler = logging.StreamHandler()
# 创建格式器
formatter = logging.Formatter('%(asctime)s [%(filename)s:%(lineno)d] %(levelname)s %(message)s',
'%Y-%m-%d %H:%M:%S')
cls.__logger.addHandler(handler)
handler.setFormatter(formatter)
return cls.__logger
在这里使用了单例设计模式,在其他用户调用该方法时,首先要判断是否已经生成了日志器,如果已经生成,那么就直接返回,如果没有,就生成一个新的日志器,这样做是为了避免每次调用时都生成一个新的日志器,方便对实例个数的控制并节约系统资源。
前期实现日志写入时,并没有判断是否只存在一个处理器(handle),这也就导致每次写入日志时,信息都会在上一次写入的次数上递增一次,导致日志的重复输出。效果如下:
2023-07-27 21:44:59 [requests_utils.py:24] ERROR 接口请求失败,原因为:ConnectionError(ProtocolError('Connection aborted.', RemoteDisconnected('Remote end closed connection without response')))
2023-07-27 21:44:59 [conftest.py:29] ERROR 获取token失败,原因为AttributeError("'NoneType' object has no attribute 'json'")
2023-07-27 21:44:59 [conftest.py:29] ERROR 获取token失败,原因为AttributeError("'NoneType' object has no attribute 'json'")
2023-07-27 21:44:59 [wrappers.py:16] INFO test_create_offer开始执行
2023-07-27 21:44:59 [wrappers.py:16] INFO test_create_offer开始执行
2023-07-27 21:44:59 [wrappers.py:16] INFO test_create_offer开始执行
2023-07-27 21:44:59 [requests_utils.py:21] INFO 接口请求成功,响应值为:{"message":"Authorization header is empty"}
2023-07-27 21:44:59 [requests_utils.py:21] INFO 接口请求成功,响应值为:{"message":"Authorization header is empty"}
2023-07-27 21:44:59 [requests_utils.py:21] INFO 接口请求成功,响应值为:{"message":"Authorization header is empty"}
2023-07-27 21:44:59 [requests_utils.py:21] INFO 接口请求成功,响应值为:{"message":"Authorization header is empty"}
2023-07-27 21:44:59 [assert_cases.py:15] ERROR 断言失败!原因是:AssertionError()
2023-07-27 21:44:59 [assert_cases.py:15] ERROR 断言失败!原因是:AssertionError()
2023-07-27 21:44:59 [assert_cases.py:15] ERROR 断言失败!原因是:AssertionError()
2023-07-27 21:44:59 [assert_cases.py:15] ERROR 断言失败!原因是:AssertionError()
2023-07-27 21:44:59 [assert_cases.py:15] ERROR 断言失败!原因是:AssertionError()
为了避免这种情况,使用if对是否存在handle进行判断,如果已经存在日志处理器了,那么就直接返回即可,不用再重新生成。
def write_case_log(func):
"""
记录用例运行日志
:param func:
:return:
"""
@wraps(func)
def wrapper(*args, **kwargs):
Logger.logger_in().info('{}开始执行'.format(func.__name__))
func(*args, **kwargs)
Logger.logger_in().info('{}执行完毕'.format(func.__name__))
return wrapper
考虑后期可能存在大量的测试用例,如果在每个测试用例中都分别加入日志输入,那么会耗费大量的精力且后期不易维护;为了便于后期的代码维护,将用例的日志输入使用装饰器实现。
class ExcelHandler:
def __init__(self, path):
self.path = path
def open_excel(self, sheet_name):
# 读取excel文件
wb = openpyxl.load_workbook(self.path)
# 获取sheet
sheet = wb[sheet_name]
wb.close()
return sheet
def get_header(self, sheet_name):
# 获取表头
sheet = self.open_excel(sheet_name)
header = []
# 遍历第一行,获取表头
for i in sheet[1]:
header_value = i.value
header.append(header_value)
return header
def read_excel(self, sheet_name):
sheet = self.open_excel(sheet_name) # 获取sheet
rows = list(sheet.rows) # 将sheet里面的每一行转换成列表,方便进行遍历
data = []
for row in rows[1:]:
row_data = []
for cell in row:
row_data.append(cell.value)
data_dict = dict(zip(self.get_header(sheet_name), row_data))
# print(data_dict) # 将每一行的值与表头关联起来
data.append(data_dict)
return data
通过使用python的openpyxl库实现excel表中测试数据的读取。首先读取并返回指定的sheet,再获取到表头,最后通过使用dict()方法将表头与数据关联起来存在字典中,方便实现接口参数的传入。
# -*- coding: gbk -*-
import yaml
def get_baseurl(path, baseurl, keyurl):
with open(path, "r", encoding="utf8") as f:
yaml_url = yaml.load(f, Loader=yaml.FullLoader)
print(yaml_url)
f.close()
return yaml_url[baseurl][keyurl]
def read_yaml(path):
with open(path, "r", encoding="utf8") as f:
yaml_ob = yaml.load(f, Loader=yaml.FullLoader)
print(yaml_ob)
f.close()
return yaml_ob
使用yaml文件,主要是为了存放获取token的数据以及测试接口所需的url地址,便于我们在测试时进行环境的切换。
首先定义get_baseurl()方法获取环境url,通过输入baseurl, keyurl来获取对应环境的url地址。
然后定义read_yaml()方法获取yaml文件中的信息,在这里将获取token所需要的数据放在yaml文件中,也是为了后期维护使用时更加方便(可以直接在pycharm中进行维护,且十分易读,结构分明)。
class RequestUtils:
session = requests.session()
def __init__(self):
self.get_url = get_baseurl("./base_url.yml", "base", "url") # 读取不同环境的url,方便切换测试环境和线上环境
def send_request(self, url, method, **kwargs):
try:
url_1 = self.get_url + url
# print(self.get_url)
# print(url_1)
Logger.logger_in().info('-----------------{}接口开始执行-----------------'.format(url))
response = RequestUtils.session.request(url=url_1, method=method, **kwargs)
Logger.logger_in().info('接口请求成功,响应值为:{}'.format(response.text))
return response
except Exception as e:
Logger.logger_in().error('接口请求失败,原因为:{}'.format(repr(e)))
return e
首先是使用了requests模块的session(),帮我们实现状态保持。(虽然目前并没有用到这样的场景,但为了框架的完整性,依旧使用了该方法,在后续涉及到需要进行状态保持的接口关联时,可以直接使用)。
定义send_request()方法,实现接口url的拼接以及接口请求的发送,并且将接口请求的情况记录到日志中。
在此处新增了一行日志输出的分隔线,由于后期实现了业务接口关联调用,一个测试用例会执行多个接口,输出分割线可以帮助我们更好的阅读日志。
2023-07-30 13:35:38 [wrappers.py:16] INFO test_create_offer开始执行
2023-07-30 13:35:38 [requests_utils.py:20] INFO -----------------/RecruitV6/api/v1/Applicant/GetApplicantIds接口开始执行-----------------
2023-07-30 13:35:38 [requests_utils.py:22] INFO 接口请求成功,响应值为:{"data":["f617a04e-ba25-4e1a-80ae-3d551e633795"],"code":200,"message":""}
2023-07-30 13:35:38 [requests_utils.py:20] INFO -----------------/dataservice/api/DataSource/GetDataSource接口开始执行-----------------
2023-07-30 13:35:38 [requests_utils.py:22] INFO 接口请求成功,响应值为:{"data":[{"key":"RecruitOnBoarding.OfferType","dataSourceResults":[{"text":"社招Offer","value":"ab56afa5-b35d-4350-8fd3-1d15eef845d4","textJson":"社招Offer","isActive":"1","isSystem":false},{"text":"校招Offer","value":"c779cf3d-28d1-48f9-989d-7396b04b1a90","textJson":"校招Offer","isActive":"1","isSystem":false},{"text":"实习offer","value":"b1fdc10d-a1fc-4be1-ba0d-32258654749a","textJson":"实习offer","isActive":"1","isSystem":false},{"text":"自定义offer1","value":"04aef502-1ac1-488d-9e4e-b874192c4a7c","textJson":"自定义offer1","isActive":"1","isSystem":false},{"text":"测试offer类型2","value":"df5826dc-6e78-4cf4-a4fd-fc1be1f88b2e","textJson":"测试offer类型2","isActive":"1","isSystem":false},{"text":"自定义offer2","value":"8690e89b-b4ca-4750-84c4-ad0103966f73","textJson":"自定义offer2","isActive":"1","isSystem":false},{"text":"测试offer类型1","value":"d54ae3fe-997a-4271-8851-33c4205b8046","textJson":"测试offer类型1","isActive":"1","isSystem":false}],"fieldName":null,"dataSourceType":8}],"code":"200","message":""}
2023-07-30 13:35:38 [requests_utils.py:20] INFO -----------------/RecruitV6/api/v1/Apply/GetApplyListByApplicantId接口开始执行-----------------
2023-07-30 13:35:38 [requests_utils.py:22] INFO 接口请求成功,响应值为:{"data":[{"applyId":"be4a15e9-97a2-4904-9cc1-160ae23c4ee1","applicantId":"f617a04e-ba25-4e1a-80ae-3d551e633795","jobId":"f70c0c9a-a4c6-4cb0-a6f0-12efbc51f2b5","processId":"d5d17dbf-814d-4f32-9cad-c76339ab3293","processPhaseId":"1f126dbf-f4e8-4e56-a7bf-bfac1426a793","processStatusId":"e15f413f-d0c0-47d4-a2ea-ff034278f6cc","fieldValues":{}}],"code":200,"message":""}
2023-07-30 13:35:38 [requests_utils.py:20] INFO -----------------/RecruitV6/api/v1/RecruitOnBoarding/CreateOfferInfo接口开始执行-----------------
2023-07-30 13:35:38 [requests_utils.py:22] INFO 接口请求成功,响应值为:{"data":"5be1ef4b-7eb6-4293-a0d4-1320788349b7","code":200,"message":null}
2023-07-30 13:35:38 [requests_utils.py:20] INFO -----------------/RecruitV6/api/v1/RecruitOnBoarding/GetOfferInfos接口开始执行-----------------
2023-07-30 13:35:39 [requests_utils.py:22] INFO 接口请求成功,响应值为:{"data":[{"id":"5be1ef4b-7eb6-4293-a0d4-1320788349b7","createBy":100,"createdTime":"2023-07-30T13:35:39.1124553","post":null,"postObj":null,"needDepartment":null,"applyChannel":null,"personFrom":null,"applyDate":"2023-05-23T13:58:34.5342975","isComplete":false,"applicantElink":null,"offerState":0,"sendApprovalTime":null,"sendOfferTime":null,"approvalState":0,"orgFullName":"","offerTypeId":"ab56afa5-b35d-4350-8fd3-1d15eef845d4","recordCreated":null,"applicantId":"f617a04e-ba25-4e1a-80ae-3d551e633795","applyId":"be4a15e9-97a2-4904-9cc1-160ae23c4ee1","modifiedBy":100,"modifiedTime":"2023-07-30T13:35:39.1124553","name":"test02","email":null,"phone":null,"phonetype":null,"idType":null,"idNumber":null,"org":null,"orgObj":null,"planDate":null,"jobId":"f70c0c9a-a4c6-4cb0-a6f0-12efbc51f2b5","jobCode":"J16571","proLine":null,"reasonStr":null,"pactTime":null,"JobGrade":null,"jobGradeObj":null,"JobRank":null,"jobRankObj":null,"probationDate":null,"probation":null,"beforeBasicPay":null,"afterBasicPay":null,"publicBasic":null,"securityBasic":null,"effectiveDate":null,"endDate":null,"signDate":null,"attachments":null,"approvalMaterials":null,"refuseReasonId":null,"acceptDate":null,"rejectDate":null,"welfare":null,"gender":null,"entryDate":null,"pdfFile":null,"signingThirdPartyDate":null,"internShipSubsidy":null,"lineManage":0,"lineManagerObj":null,"dotManage":0,"dotManagerObj":null,"beginWorkDate":null,"workPlace":null,"recruitRequirementLookup":null,"permissionField":null,"extendInfos":[{"text":"","name":"Onwer","value":100},{"text":"","name":"extssdfgfghhj4356455_410000_1950780832","value":null},{"text":"","name":"extextznjyf606773839295707_410000_1316167470","value":null},{"text":"","name":"extdsf54343535453_410000_401361418","value":null},{"text":null,"name":"exte3434_410000_432447782","value":null},{"text":null,"name":"extzhuanzhenghougangweigongzi_410000_1381981686","value":null},{"text":null,"name":"extzonghegongshijiabanbutieshiyongqi_410000_740884613","value":null},{"text":null,"name":"extyuexinzongjishiyongqi_410000_460566386","value":null},{"text":"","name":"extpicUpload_410000_716039143","value":null},{"text":null,"name":"extyuebiaozhungongzizhuanzheng_410000_1200505899","value":null},{"text":"","name":"extdsf43543_410000_375238909","value":null},{"text":null,"name":"extccshikaifa_410000_1584751792","value":null},{"text":null,"name":"extjixiaogongzizhuanzheng_410000_949177300","value":null},{"text":null,"name":"extxinzo_410000_568905541","value":null},{"text":"","name":"extasiug_410000_1417512741","value":null},{"text":"","name":"extshifoulanlinggongrne_410000_35014856","value":null},{"text":null,"name":"extlookup11_410000_218776234","value":null},{"text":"","name":"extaaa_410000_916735768","value":null},{"text":"","name":"extextdebje6067731259604019_410000_519697908","value":null},{"text":null,"name":"extextrzdsydybff6067731961161106_410000_518818249","value":null},{"text":null,"name":"extjixiaogongzishiyongqi_410000_1510979931","value":null},{"text":null,"name":"extextffbs6067731166350017_410000_511507282","value":null},{"text":null,"name":"extyuexinzongjizhuanzheng_410000_1981787164","value":null},{"text":"","name":"extavc_410000_1509548279","value":null},{"text":"","name":"extfileUpload_410000_545164225","value":null},{"text":"","name":"extextcqjl6067731996166332_410000_2014021631","value":null},{"text":null,"name":"extzonghegongshijiabanbutiezhuanzheng_410000_691705984","value":null},{"text":"","name":"extfdgdfgdfg43345_410000_1276538596","value":null},{"text":null,"name":"extgangweigongzizhuanzheng_410000_1001575836","value":null},{"text":null,"name":"extcustmult01_410000_1805096460","value":null},{"text":"","name":"extextzgdlj60677317683_410000_1339661530","value":null},{"text":"","name":"extextsfyzfbt606773224919108_410000_1532692855","value":null},{"text":"","name":"extextsfyznjyf60677353100989_410000_622433298","value":null},{"text":"","name":"extextqzf606773631522154_410000_617219108","value":null},{"text":"","name":"extmultiPicUpload_410000_1745111356","value":null},{"text":null,"name":"extjintie234_410000_438160583","value":null},{"text":"","name":"extextsfycqjl606773667001049_410000_399093849","value":null},{"text":null,"name":"extdfg234_410000_696428294","value":null},{"text":null,"name":"extextrzdsydebff606773262473866_410000_642512888","value":null},{"text":"","name":"extmultiFileUpload_410000_512503722","value":null},{"text":"","name":"extdsf4535_410000_1974556573","value":null},{"text":null,"name":"extjibengongzizhuanzheng_410000_1864473652","value":null},{"text":"","name":"extextdybje6067731461268998_410000_1909313268","value":null},{"text":"","name":"extextljbz606773376330165_410000_572350980","value":null},{"text":null,"name":"extfuwenben_410000_1787459387","value":null},{"text":null,"name":"extzonghe1232112_410000_2138547005","value":null},{"text":null,"name":"extgangweigongzishiyongqi_410000_1219379","value":null},{"text":"","name":"extdanxuanliebiao_410000_719220132","value":null},{"text":"","name":"extll_410000_1075927860","value":null},{"text":"","name":"extextspr3_410000_790676610","value":null},{"text":"","name":"extsdfds3454_410000_1893554040","value":null},{"text":"","name":"extyuebiaozhungongzi2_410000_1996804789","value":null},{"text":"","name":"extextzfbt6067731799027952_410000_671908912","value":null},{"text":"","name":"extextsfyqzf606773282718948_410000_2041956697","value":null},{"text":null,"name":"extjibengongzishiyongqi_410000_3216026","value":null},{"text":null,"name":"exto0p0_410000_515485633","value":null},{"text":null,"name":"extyuebiaozhungongzishiyongqi_410000_1412658204","value":null},{"text":"","name":"extdfds4356456_410000_2126743478","value":null},{"text":"","name":"extextsfyljbz606773858283801_410000_629856499","value":null},{"text":"","name":"extdsfd435_410000_1216408346","value":null}],"approvalInfos":null,"fileInfos":[{"name":"Attachment","text":"发送附件给应聘者","downloadUrls":[],"clientUrls":[]},{"name":"PdfFile","text":"Offer附件","downloadUrls":[],"clientUrls":[]},{"name":"ApprovalMaterials","text":"发送附件给审批人","downloadUrls":[],"clientUrls":[]},{"name":"extpicUpload_410000_716039143","text":"","downloadUrls":[],"clientUrls":[]},{"name":"extfileUpload_410000_545164225","text":"","downloadUrls":[],"clientUrls":[]},{"name":"extmultiPicUpload_410000_1745111356","text":"","downloadUrls":[],"clientUrls":[]},{"name":"extmultiFileUpload_410000_512503722","text":"","downloadUrls":[],"clientUrls":[]}],"additionalInfos":null}],"code":200,"message":null}
2023-07-30 13:35:39 [assert_cases.py:12] INFO 断言成功!
2023-07-30 13:35:39 [wrappers.py:18] INFO test_create_offer执行完毕
在我们新建offer时,需要使用到applicantid、applyif、offerTypeid,在此处,我分别以接口所属的对象对接口进行封装,将接口封装完毕后,后续我们执行测试用例就调用封装的方法即可。
以offer对象为例做以下说明:
import json
from utils.read_yaml import read_yaml
from utils.requests_utils import RequestUtils
class Offer:
# 获取offer类型id
@staticmethod
def get_offer_type(path, get_headers):
data = read_yaml(path)
# print(json.dumps(header))
response = RequestUtils().send_request(method=data["method"], url=data["url"], data=json.dumps(data["body"]),
headers=get_headers)
# print(response.request.body)
response = json.loads(response.text)
# print(data['data'][0]["dataSourceResults"][0]["value"])
# return data['data'][0]["dataSourceResults"][0]["value"]
return response
# 创建offer
@staticmethod
def create_offer(data, headers, get_offer_type, get_applicant_id, get_apply_id):
# print(data)
# print(data["json"])
# body = json.dumps(data["json"])
# json.loads(body)
# 将json的值从字符串转换成字典类型
body = eval(data["json"])
body["offerTypeID"] = get_offer_type
body["applicantId"] = get_applicant_id
body["applyId"] = get_apply_id
response = RequestUtils().send_request(url=data["url"], method=data["method"],
json=body, headers=headers)
response = json.loads(response.text)
return response
# 根据offerid查询offer
@staticmethod
def get_offer(path, header, offer_id):
data = read_yaml(path)
offer_id = str(offer_id)
body = [offer_id]
response = RequestUtils().send_request(url=data['url'], method=data["method"], json=body, headers=header)
return response
在此处,按照offer对象,对获取offer类型、创建offer、查询offer三个OpenAPI接口实现了接口封装。
在我们编写测试用例时,可直接调用封装的接口,更好的实现接口串联。
import json
import os
import allure
import pytest
from common.get_data import GetData
from common.wrappers import write_case_log
from common.assert_cases import Assert
from utils.requests_utils import RequestUtils
def get_data():
return GetData.get_data(os.path.basename(__file__))
class TestOffer:
@write_case_log
@allure.title("{data[title]}") # 命名用例名称方式1
@allure.severity(allure.severity_level.CRITICAL)
@pytest.mark.parametrize("data", get_data())
def test_create_offer(self, data, get_headers):
applicant_id = Applicant.get_applicant_id('./data/get_applicant.yml', get_headers)
create_offer_response = Offer.create_offer(data, get_headers,
Offer.get_offer_type('./data/get_type.yml', get_headers)['data'][0]["dataSourceResults"][0]["value"],
applicant_id,
Apply.get_apply_id('./data/get_applyid.yml', applicant_id,
get_headers))
# print(response.text)
response = Offer.get_offer('./data/get_offer.yml', get_headers, create_offer_response["data"])
# print(response)
response = json.loads(response.text)
# 若能根据新建的offer的offerid能查询到对应的offer,那么说明新建offer成功
Assert.custom_assert(create_offer_response["data"], response["data"][0]["id"])
由于在前面已经对各个模块所需要的方法进行了封装,到测试用例时,直接调用封装的方法即可,已经变得十分简单且易于维护了。
在测试用例中值得关注的地方是使用的pytest的参数化,通过使用 @pytest.mark.parametrize()实现数据驱动,首先我们看一下get_data()获取到的测试数据:
[{'url': '/RecruitV6/api/v1/RecruitOnBoarding/CreateOfferInfo', 'method': 'post', 'json': '{"applyId": "a695a05d-4244-4672-b165-d2c8021eae1b","applicantId":"f36f2a64-362b-4e9b-a8b9-68b2525154d7","offerTypeID": "df5826dc-6e78-4cf4-a4fd-fc1be1f88b2e","name": "test01"}', 'code': 200, 'title': '创建Offer成功'},
{'url': '/RecruitV6/api/v1/RecruitOnBoarding/CreateOfferInfo', 'method': 'post', 'json': '{"applyId": "a695a05d-4244-4672-b165-d2c8021eae1b","applicantId":"f36f2a64-362b-4e9b-a8b9-68b2525154d7","offerTypeID": "df5826dc-6e78-4cf4-a4fd-fc1be1f88b2e","name": "test02"}', 'code': 200, 'title': '创建Offer成功'}]
是一个列表里面嵌套了两个字典,对应的分别是两条测试用例,@pytest.mark.parametrize()会根据列表中的两个字典(一个参数多个值),执行两次测试用例。
创建offer接口的响应信息:
test_cases/test_Offer.py::TestOffer::test_create_offer[data0]
{"data":"6917b415-f15f-4115-8080-f370e8598694","code":200,"message":null}
test_cases/test_Offer.py::TestOffer::test_create_offer[data1]
{"data":"a33b1e8e-54b2-4515-ad27-5ac9eb036538","code":200,"message":null}
在创建offer成功后,拿到新创建的offer的offerid进行查询,若能查询到对应的offerid,则说明创建offer成功,测试用例执行通过。
test_OpenAPI/test_cases/test_Offer.py::TestOffer::test_create_offer[data0] PASSED
test_OpenAPI/test_cases/test_Offer.py::TestOffer::test_create_offer[data1] PASSED
运行完成后,使用allure生成测试报告。