diff --git a/README.md b/README.md index 8b20b9e..19eeca4 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,9 @@ https://www.bilibili.com/video/BV1EE411B7SU?p=10 2020/11/21 更新用例文档,合并文件对象,文件地址,优化文件上传处理方式 2020/11/21 config.yaml文件中新增request_headers 选项,默认header在此设置,优化test_api.py文件,整合read_file.py + +2020/11/22 优化请求断言方法支持用户自定义提取响应自定内容,支持多数据断言,整合请求方法,优化测试启动方法,部分日志移除,修改预期结果处理 + #### 博客园首发 https://www.cnblogs.com/zy7y/p/13426816.html diff --git a/api/base_requests.py b/api/base_requests.py index 0efd363..8e61c26 100644 --- a/api/base_requests.py +++ b/api/base_requests.py @@ -7,59 +7,79 @@ @ide: PyCharm @time: 2020/7/31 """ -from loguru import logger -import requests -from tools import convert_json +import requests +from tools import allure_step, allure_title, logger, extractor from tools.data_process import DataProcess +from tools.read_file import ReadFile class BaseRequest(object): - def __init__(self): - # 修改时间:2020年9月14日17:09 - # 确保,整个接口测试中,使用同一个requests.Session() 来管理cookie - self.session = requests.Session() + session = None - # 请求 - def api_send(self, method, url, parametric_key=None, data=None, file_obj=None, header=None): + @classmethod + def get_session(cls): + if cls.session is None: + cls.session = requests.Session() + return cls.session + + @classmethod + def send_request(cls, case: list, env: str = 'dev') -> object: + """处理case数据,转换成可用数据发送请求 + :param case: 读取出来的每一行用例内容,可进行解包 + :param env: 环境名称 默认使用config.yaml server下的 dev 后面的基准地址 + return: 响应结果, 预期结果 """ + case_number, case_title, path, token, method, parametric_key, file_obj, data, expect = case + # allure报告 用例标题 + allure_title(case_title) + # 处理url、header、data、file、的前置方法 + url = ReadFile.read_config(f'$.server.{env}') + DataProcess.handle_path(path) + allure_step('请求地址', url) + header = DataProcess.handle_header(token) + allure_step('请求头', header) + data = DataProcess.handle_data(data) + allure_step('请求参数', data) + file = DataProcess.handler_files(file_obj) + allure_step('上传文件', file) + # 发送请求 + res = cls.send_api(url, method, parametric_key, header, data, file) + allure_step('响应耗时(s)', res.elapsed.total_seconds()) + allure_step('响应内容', res.json()) + # 响应后操作 + if token == '写': + DataProcess.have_token['Authorization'] = extractor(res.json(), ReadFile.read_config('$.expr.token')) + allure_step('请求头中添加Token', DataProcess.have_token) + DataProcess.save_response(case_number, res.json()) + allure_step('存储实际响应', DataProcess.response_dict) + return res.json(), expect + @classmethod + def send_api(cls, url, method, parametric_key, header=None, data=None, file=None) -> object: + """ :param method: 请求方法 :param url: 请求url :param parametric_key: 入参关键字, get/delete/head/options/请求使用params, post/put/patch请求可使用json(application/json)/data - :param data: 参数数据,默认等于None - :param file_obj: 文件对象的地址, 单个文件直接放地址:/Users/zy7y/Desktop/vue.js - 多个文件格式:["/Users/zy7y/Desktop/vue.js","/Users/zy7y/Desktop/jenkins.war"] + :param file: 文件对象 :param header: 请求头 - :return: 返回json格式的响应 + :return: 返回res对象 """ - # 修改时间:2020年9月14日17:09 - session = self.session - - files = DataProcess.handler_files(file_obj) + session = cls.get_session() if parametric_key == 'params': - logger.info(f'{method, url, data, header}') res = session.request(method=method, url=url, params=data, headers=header) elif parametric_key == 'data': - res = session.request(method=method, url=url, data=data, files=files, headers=header) + res = session.request(method=method, url=url, data=data, files=file, headers=header) elif parametric_key == 'json': - res = session.request(method=method, url=url, json=data, files=files, headers=header) + res = session.request(method=method, url=url, json=data, files=file, headers=header) else: raise ValueError( '可选关键字为:get/delete/head/options/请求使用params, post/put/patch请求可使用json(application/json)/data') - logger.info(f'请求方法:{method},请求路径:{url}, 请求参数:{data}, 请求文件:{files}, 请求头:{header})') - return res.json() - - def api_front(self): - """请求api的前置处理方法""" - pass - - def api_position(self): - """请求api的后置处理方法""" - pass + logger.info(f'\n请求地址:{url}\n请求方法:{method}\n请求头:{header}\n请求参数:{data}\n响应数据:{res.json()}') + return res + diff --git a/config/config.yaml b/config/config.yaml index 60214ee..00f76b5 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -11,18 +11,15 @@ request_headers: User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36 -# 实际响应jsonpath提取规则设置 +# 提取规则设置 expr: # 提取token的jsonpath表达式 token: $.data.token - # 提取实际响应的断言数据jsonpath表达式,与excel中预期结果的数据进行比对用 - response: $.meta file_path: - case_data: ../data/case_data.xlsx - report_data: ../report/data/ - report_generate: ../report/html/ - log_path: ../log/运行日志{time}.log + test_case: ../data/case_data.xlsx + report: ../report/ + log: ../log/run{time}.log email: # 发件人邮箱 @@ -35,6 +32,4 @@ email: # 收件人邮箱 addressees: ["收件人邮箱1","收件人邮箱2","收件人邮箱3"] title: 接口自动化测试报告(见附件) - # 附件地址 - enclosures: ["../report/html/apiAutoTestReport.zip",] diff --git a/data/case_data.xlsx b/data/case_data.xlsx index 77f7b67..5e91d1d 100644 Binary files a/data/case_data.xlsx and b/data/case_data.xlsx differ diff --git a/test/test_api.py b/test/test_api.py index ad82e67..ea95b54 100644 --- a/test/test_api.py +++ b/test/test_api.py @@ -1,87 +1,47 @@ -#!/usr/bin/env/python3 +#!/usr/bin/env/ python3 # -*- coding:utf-8 -*- """ @project: apiAutoTest @author: zy7y @file: test_api.py @ide: PyCharm -@time: 2020/7/31 +@time: 2020/11/22 +@desc: 测试方法 """ +import os +import shutil + import pytest + +from tools import logger from api.base_requests import BaseRequest -from tools import * from tools.data_process import DataProcess from tools.read_file import ReadFile -base_url = ReadFile.read_config('$.server.dev') -res_reg = ReadFile.read_config('$.expr.response') -report_data = ReadFile.read_config('$.file_path.report_data') -report_generate = ReadFile.read_config('$.file_path.report_generate') -log_path = ReadFile.read_config('$.file_path.log_path') +report = ReadFile.read_config('$.file_path.report') +logfile = ReadFile.read_config('$.file_path.log') # 读取excel数据对象 -data_list = ReadFile.read_testcase() -# 请求对象 -br = BaseRequest() -logger.info(f'配置文件/excel数据/对象实例化,等前置条件处理完毕\n\n') +cases = ReadFile.read_testcase() -class TestApiAuto(object): - # 启动方法 - def run_test(self): - import os, shutil - if os.path.exists('../report') and os.path.exists('../log'): +class TestApi: + + @classmethod + def run(cls): + if os.path.exists('../report'): shutil.rmtree(path='../report') - shutil.rmtree(path='../log') - # 日志存取路径 - logger.add(log_path, encoding='utf-8') - pytest.main(args=[f'--alluredir={report_data}']) - # 本地生成 allure 报告文件,需注意 不用pycharm等类似ide 打开会出现无数据情况 - os.system(f'allure generate {report_data} -o {report_generate} --clean') + logger.add(logfile, enqueue=True, encoding='utf-8') + logger.info('开始测试...') + pytest.main(args=[f'--alluredir={report}/data']) + os.system(f'allure generate {report}/data -o {report}/html --clean') + logger.success('报告已生成') - # 直接启动allure报告(会占用一个进程,建立一个本地服务并且自动打开浏览器访问,ps 程序不会自动结束,需要自己去关闭) - # os.system(f'allure serve {report_data}') - logger.warning('报告已生成') - - @pytest.mark.parametrize('case_number,case_title,path,is_token,method,parametric_key,file_obj,' - 'data,expect', data_list) - def test_main(self, case_number, case_title, path, is_token, method, parametric_key, file_obj, - data, expect): - logger.debug(f'⬇️⬇️⬇️...执行用例编号:{case_number}...⬇️⬇️⬇️️') - - allure_title(case_title) - path = DataProcess.handle_path(path) - allure_step('请求地址', base_url + path) - allure_step('请求方式', method) - header = DataProcess.handle_header(is_token) - allure_step('请求头', header) - data = DataProcess.handle_data(data) - allure_step('请求数据', data) - res = br.api_send(method=method, url=base_url + path, parametric_key=parametric_key, file_obj=file_obj, - data=data, header=header) - allure_step('实际响应结果', res) - DataProcess.save_response(case_number, res) - allure_step('响应结果写入字典', DataProcess.response_dict) - # 写token的接口必须是要正确无误能返回token的 - if is_token == '写': - DataProcess.have_token['Authorization'] = extractor(res, ReadFile.read_config('$.expr.token')) - really = extractor(res, res_reg) - allure_step('根据配置文件的提取响应规则提取实际数据', really) - expect = convert_json(expect) - allure_step('处理读取出来的预期结果响应', expect) - allure_step('响应断言', (really == expect)) - - assert really == expect + @pytest.mark.parametrize('case', cases) + def test_main(self, case): + response, expect = BaseRequest.send_request(case) + # 断言操作 + DataProcess.assert_result(response, expect) if __name__ == '__main__': - TestApiAuto().run_test() - - # 使用jenkins集成将不会使用到这两个方法(邮件发送/报告压缩zip) - # from tools.zip_file import zipDir - # from tools.send_email import send_email - # zipDir(report_generate, report_zip) - # send_email(email_setting) - - - - + TestApi.run() \ No newline at end of file diff --git a/tools/__init__.py b/tools/__init__.py index cda7e82..7ada494 100644 --- a/tools/__init__.py +++ b/tools/__init__.py @@ -38,10 +38,8 @@ def rep_expr(content: str, data: dict, expr: str = '&(.*?)&') -> str: :param expr: 查找用的正则表达式 return content: 替换表达式后的字符串 """ - logger.info(f'替换前内容{content}') for ctt in re.findall(expr, content): content = content.replace(f'&{ctt}&', str(extractor(data, ctt))) - logger.info(f'替换后内容{content}') return content @@ -67,7 +65,6 @@ def convert_json(dict_str: str) -> dict: dict_str = dict_str.replace('false', 'False') dict_str = eval(dict_str) logger.error(e) - logger.info(f'{dict_str}, {type(dict_str)}') return dict_str @@ -82,8 +79,4 @@ def allure_step(step: str, var: str) -> None: :param var: 附件内容 """ with allure.step(step): - allure.attach(json.dumps(var, ensure_ascii=False, indent=4), step, allure.attachment_type.TEXT) - - -if __name__ == '__main__': - print(convert_json('["1","2"]')) \ No newline at end of file + allure.attach(json.dumps(var, ensure_ascii=False, indent=4), step, allure.attachment_type.TEXT) \ No newline at end of file diff --git a/tools/data_process.py b/tools/data_process.py index 9898d9c..547b75e 100644 --- a/tools/data_process.py +++ b/tools/data_process.py @@ -7,16 +7,13 @@ @ide: PyCharm @time: 2020/11/18 """ -from tools import * +from tools import logger, extractor, convert_json, rep_expr, allure_step from tools.read_file import ReadFile class DataProcess: response_dict = {} header = ReadFile.read_config('$.request_headers') - - logger.error(header) - have_token = header.copy() @classmethod @@ -57,7 +54,7 @@ class DataProcess: 实例- 单个文件: &file&D: """ if file_obj == '': - return None + return for k, v in convert_json(file_obj).items(): # 多文件上传 if isinstance(v, list): @@ -79,5 +76,21 @@ class DataProcess: return data = rep_expr(variable, cls.response_dict) variable = convert_json(data) - logger.info(f'最终的请求数据如下: {variable}') return variable + + @classmethod + def assert_result(cls, response: dict, expect_str: str): + """ 预期结果实际结果断言方法 + :param response: 实际响应字典 + :param expect_str: 预期响应内容,从excel中读取 + return None + """ + expect_dict = convert_json(expect_str) + index = 0 + for k, v in expect_dict.items(): + actual = extractor(response, k) + index += 1 + logger.info(f'第{index}个断言,实际结果:{actual} | 预期结果:{v} \n断言结果 {actual == v}') + allure_step(f'第{index}个断言', f'实际结果:{actual} = 预期结果:{v}') + assert actual == v + diff --git a/tools/read_file.py b/tools/read_file.py index 43efc28..f6eebf3 100644 --- a/tools/read_file.py +++ b/tools/read_file.py @@ -10,14 +10,13 @@ """ import yaml import xlrd -from tools import * +from tools import extractor class ReadFile: config_dict = None @classmethod - @logger.catch def get_config_dict(cls, config_path: str = '../config/config.yaml') -> dict: """读取配置文件,并且转换成字典 :param config_path: 配置文件地址, 默认使用当前项目目录下的config/config.yaml @@ -30,7 +29,6 @@ class ReadFile: return cls.config_dict @classmethod - @logger.catch def read_config(cls, expr: str = '.') -> dict: """默认读取config目录下的config.yaml配置文件,根据传递的expr jsonpath表达式可任意返回任何配置项 :param expr: 提取表达式, 使用jsonpath语法,默认值提取整个读取的对象 @@ -39,14 +37,13 @@ class ReadFile: return extractor(cls.get_config_dict(), expr) @classmethod - @logger.catch def read_testcase(cls): """ 读取excel格式的测试用例 :return: data_list - pytest参数化可用的数据 """ data_list = [] - book = xlrd.open_workbook(cls.read_config('$.file_path.case_data')) + book = xlrd.open_workbook(cls.read_config('$.file_path.test_case')) # 读取第一个sheet页 table = book.sheet_by_index(0) for norw in range(1, table.nrows): @@ -55,11 +52,5 @@ class ReadFile: value = table.row_values(norw) value.pop(3) data_list.append(list(value)) - logger.info(f'{data_list}') return data_list - -if __name__ == '__main__': - ReadFile.read_config() - ReadFile.read_testcase() - diff --git a/tools/send_email.py b/tools/send_email.py index c6e07e9..9b4b025 100644 --- a/tools/send_email.py +++ b/tools/send_email.py @@ -8,7 +8,7 @@ @time: 2020/8/3 """ import yagmail -from loguru import logger +from tools import logger def send_email(setting): @@ -25,7 +25,7 @@ def send_email(setting): """ yag = yagmail.SMTP(setting['user'], setting['password'], setting['host']) # 发送邮件 - yag.send(setting['addressees'], setting['title'], setting['contents'], setting['enclosures']) + yag.send(setting['addressees'], setting['title'], setting['contents']) # 关闭服务 yag.close() logger.info("邮件发送成功!") \ No newline at end of file