diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..35410ca --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..1208c89 --- /dev/null +++ b/Pipfile @@ -0,0 +1,29 @@ +[[source]] +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +verify_ssl = true +name = "pypi" + +[packages] +allure-pytest = "==2.11.1" +faker = "==15.1.1" +pandas = "*" +pymysql = "==1.0.2" +pytest = "==7.2.0" +pytest-ordering = "==0.6" +pytest-rerunfailures = "==11.1" +pyyaml = "==6.0" +requests = "==2.28.1" +click = "==8.1.3" +urllib3 = "==1.26.12" +loguru = "==0.6.0" +openpyxl = "==3.1.0" +ddddocr = "==1.4.7" +python-jenkins = "*" +dingtalkchatbot = "1.5.7" +pydantic = "==1.10.5" +jsonpath = "==0.82" + +[dev-packages] + +[requires] +python_version = "3.8" diff --git a/README.md b/README.md new file mode 100644 index 0000000..95bf289 --- /dev/null +++ b/README.md @@ -0,0 +1,158 @@ +**EasyTest接口自动化测试框架** +=== +## 框架模式 +基于 pytest + allure + yaml + mysql + 钉钉通知 + Jenkins 实现的接口自动化框架 +* git地址: [https://gitee.com/t_l_h/EasyTest.git](https://gitee.com/t_l_h/EasyTest.git) + +## 框架介绍 +为了抛弃臃肿庞大的测试框架,EasyTest将大部分代码逻辑通过conftest文件实现前置,使得编写测试用例时无需导入各种乱七八糟的模块。 + +## 框架功能 +* yaml管理测试数据,实现测试数据分离 +* 支持不同接口间的数据依赖 +* 支持数据库的增、删、改、查 +* 支持yaml文件中的动态参数自动替换 +* 支持测试完成后发送钉钉消息通知 +* 可轻易集成jenkins + +## 遇到问题 +* 可以直接提issue或联系v:Tlh_0717 + + +## 目录结构 +``` + ├── files 存放接口上传的文件 + ├── test_cases 存放测试用例 + ├── test_data yaml格式测试数据 + ├── utils 存放各种封装方法 + ├── conftest.py pytest钩子函数 + ├── pytest.ini pytest执行脚本参数配置文件 + ├── run.py 全量执行用例 + ├── send_ding.py 钉钉消息通知 + ├── config.yml 全局配置 + ├── requirements.txt 依赖文件 + └── Pipfile 虚拟环境依赖文件 +``` +### 使用教程 + +#### 1、Gitee 拉取项目 + +需要先配置好python、jdk、allure环境(不懂的自行百度) + +```shell +git clone https://gitee.com/tanlinhai_code/EasyTest.git +``` + +#### 2、安装依赖 + +方式一: +直接安装依赖文件 + +```shell +pip install -r requirements.txt +``` + +方式二: +使用pipenv创建虚拟环境运行 +```shell +"""配置虚拟环境""" + +pipenv install # 根据根目录下的Pipfile创建一个新环境 + +pipenv --venv # 查看虚拟环境路径 + +/Users/用户/.local/share/virtualenvs/api_autotest-J3yMsRGU # 创建的虚拟环境地址 +``` + +```shell +"""激活虚拟环境""" + +pipenv shell # 激活虚拟环境 + +exit # 退出虚拟环境 + +``` + +## 配置项目的通用配置 +![img.png](files/testconfig.png) + + + +## 编写测试用例 + +### 创建yaml测试文件 +![img.png](files/testdata.png) + +在test_data目录下的login目录中创建yaml文件,注:必须是二级目录下创建,通常业务也会划分模块。 + +字段说明: + +* common_inputs: 请求方法,请求路径(只需要写域名后的路径即可) +* case: 用例名 +* inputs: 用例输入 +* params: 请求为get类型时填写 +* json: 请求为post类型时填写 +* file: 请求上传的文件名,文件需要放在files目录下 +* sql: sql语句 +* expectation: 用例输出 +* response: 接口返回数据体 + +创建好了yaml文件,就可以创建测试用例文件了 +### 创建测试用例文件 +![img.png](files/testcase.png) + +同样的,在test_cases下的login目录中创建yaml文件,目录层级与test_data保持一致 + +字段说明: + +* allure.feature: 模块名称 + +* allure.title: 用例名称 + +* pytest.mark.imports 用imports标记这条用例,后续可以执行指定标记的用例 + +* pytest.mark.datafile: 需要使用的yaml测试数据(需要从test_data目录开始写,千万不能写错) + +* 测试函数必须test_*开头,重点说各个入参的字段 + +* requests: 封装好的实例化请求方法,根据请求方式直接调用requests.post或requests.get,如图中的res所示 + +* url: 由根目录config.yml文件中读取的路由及该测试用例对应的yaml文件中读取的path拼接,无需填写,只需正常传入 + +* headers: 以静态方法存放在utils.requests_control文件的RestClient类中,可按需更改 + +* case: 用例对应yaml文件中的case名称,可在做不同校验时作为判断条件 + +* inputs: 用例对应yaml文件中inputs内容,使用时直接用获取字典值的方式获取, + + 1、如果是上传文件的接口,可以参考前面截图中的样式填写,只需要填写files目录下的文件名即可 + + 2、如果请求参数需要动态参数,需要用"{{xxx}}"这种双引号和双括号包裹的方式填写,名称必须是utils.fake_data_control.py文件Execute类中定义的方法名,可以按需添加 + +* expectation: 用例对应yaml文件中expectation内容,使用时可以直接用字典值方式获取 + +* sql: 数据库实例化方法,可以通过sql.query('xxxx'), sql.execute('xxxxx')方式调用,使用时直接使用yaml文件中的sql语句:sql.query(inputs['sql'])(当然也可以直接写) + +* cache: 共享的缓存数据,这个使用起来主要注意:例如在test_1中我需要使用某个数据给test_2使用,那么可以在test_1中使用cache['key'] = 1 这样的方式把数据存入全局的共享数据中,test_2使用时就用字典取值的方式cache['key']即可 +,那么就有人问了,那多个数据依赖怎么办呢?目前的办法是将会产出依赖数据的接口打上@pytest.mark.run(order=1)的标签,并且存入共享数据时的key必须不重复, +这样可以理解为用例执行开始前有一个空字典,在测试过程中不断向其中添加依赖数据,而在后面执行的用例只需要从里面取就可以了 + +### 运行测试用例 + +```shell + +python run.py # 全量执行用例,生成测试报告保存在allure-report目录下 + +pipenv run.py -m login # 执行被@pytest.mark.login标记的所有用例 + +python send_ding.py # 发送测试结果到钉钉通知,如果jenkins和config.yml都配置好了,可以直接使用 + +``` + +钉钉通知样式: + +![img.png](files/testding.png) + + +## 最后 +框架还有许多能优化的地方,如果有什么好的建议欢迎一起来讨论。 diff --git a/__pycache__/conftest.cpython-38-pytest-7.1.2.pyc b/__pycache__/conftest.cpython-38-pytest-7.1.2.pyc new file mode 100644 index 0000000..4373859 Binary files /dev/null and b/__pycache__/conftest.cpython-38-pytest-7.1.2.pyc differ diff --git a/__pycache__/conftest.cpython-38-pytest-7.2.0.pyc b/__pycache__/conftest.cpython-38-pytest-7.2.0.pyc new file mode 100644 index 0000000..35737e1 Binary files /dev/null and b/__pycache__/conftest.cpython-38-pytest-7.2.0.pyc differ diff --git a/config.yml b/config.yml new file mode 100644 index 0000000..a859917 --- /dev/null +++ b/config.yml @@ -0,0 +1,35 @@ +project_name: 项目名称 +env: 测试环境 +tester_name: xxx + +# 测试域名 +host: https://xxxxx.com + +# 登录用户信息 +account: xxxxxxx +password: xxxxxxxxxxxxx + + +# jenkins相关配置 +jenkins: + # 本地服务 + url: http://127.0.0.1:8080 + # 如果有映射网址,可以填在这里 + mapping_url: xxxxxxx + # jenkins账号 + user: + # jenkins密码 + pwd: + # 要推送的项目名 + project: + +# 钉钉消息通知相关配置 +ding_talk: + webhook: + +# 数据库相关配置 +mysql_db: + host: + user: + password: + port: \ No newline at end of file diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..2d2f2ae --- /dev/null +++ b/conftest.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import os +import time +import pytest +from utils.path import root +from itertools import zip_longest +from utils.database_control import MysqlDB +from utils.fake_data_control import Execute +from utils.requests_control import RestClient +from utils.open_file_control import open_file +from utils.read_yaml_control import HandleYaml + + +def pytest_generate_tests(metafunc): + """参数化测试函数""" + + case, url, inputs, expectation = [], [], [], [] + markers = metafunc.definition.own_markers + for marker in markers: + if marker.name == 'datafile': + test_data_path = os.path.join(metafunc.config.rootdir, marker.args[0]) if marker.args else ... + test_data = HandleYaml(test_data_path).get_yaml_data() + # if 'cases' in metafunc.fixturenames: + for data in test_data['tests']: + for k, v in list(data['inputs']['json'].items()): + if str(v)[0:2] == '{{' and str(v)[-2:] == '}}': + val = str(v)[2:-2] + data['inputs']['json'][k] = Execute(val)() # 替换yaml文件json中{{xxx}}xxx的值 + if data['inputs']['file']: + file_name = str(data['inputs']['file']) + data['inputs']['file'] = eval(f"open_file(f'{str(file_name)}')") # 直接返回open_file对象 + case.append(data['case'] if data['case'] is not None else {}) + url.append(str(host()) + str(test_data['common_inputs']['path']) if test_data['common_inputs'][ + 'path'] is not None else {}) + inputs.append(data['inputs'] if data['inputs'] is not None else {}) + expectation.append(data['expectation'] if data['expectation'] is not None else {}) + metafunc.parametrize("case, url, inputs, expectation", zip_longest(case, url, inputs, expectation), + scope='function') + + +def pytest_collection_modifyitems(items): + """测试用例收集完成时,将收集到的item的name和nodeid的中文显示""" + + for item in items: + item.name = item.name.encode("utf-8").decode("unicode_escape") + item._nodeid = item.nodeid.encode("utf-8").decode("unicode_escape") + + +@pytest.fixture() +def headers(): + """通用请求头""" + + yield RestClient().headers() + + +def host(): + """获取配置文件中的域名""" + + res_host = HandleYaml(root / 'config.yml').get_yaml_data()['host'] + return res_host + + +@pytest.fixture() +def requests(): + """返回实例化请求方法""" + + yield RestClient() + + +@pytest.fixture() +def sql(): + """返回实例化数据库方法""" + + yield MysqlDB() + + +@pytest.fixture(scope='session') +def cache(): + """返回一个字典,用作数据共享""" + + yield {} diff --git a/files/avatar_new.png b/files/avatar_new.png new file mode 100644 index 0000000..b2474af Binary files /dev/null and b/files/avatar_new.png differ diff --git a/files/import.xlsx b/files/import.xlsx new file mode 100644 index 0000000..f07774e Binary files /dev/null and b/files/import.xlsx differ diff --git a/files/testcase.png b/files/testcase.png new file mode 100644 index 0000000..9cee54b Binary files /dev/null and b/files/testcase.png differ diff --git a/files/testconfig.png b/files/testconfig.png new file mode 100644 index 0000000..768a889 Binary files /dev/null and b/files/testconfig.png differ diff --git a/files/testdata.png b/files/testdata.png new file mode 100644 index 0000000..41a9097 Binary files /dev/null and b/files/testdata.png differ diff --git a/files/testding.png b/files/testding.png new file mode 100644 index 0000000..3307187 Binary files /dev/null and b/files/testding.png differ diff --git a/img.png b/img.png new file mode 100644 index 0000000..f44c587 Binary files /dev/null and b/img.png differ diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..0101d8a --- /dev/null +++ b/pytest.ini @@ -0,0 +1,8 @@ +[pytest] +addopts = -p no:warnings -s +filterwarnings = ignore:Module already imported:pytest.PytestWarning +markers = + datafile: hook marker + login: login + resource: resource + user: user \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3f3222c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,20 @@ +-i https://pypi.tuna.tsinghua.edu.cn/simple + +allure-pytest==2.11.1 +Faker==15.1.1 +pandas==1.5.3 +openpyxl==3.1.0 +PyMySQL==1.0.2 +pytest==7.2.0 +pytest-ordering==0.6 +pytest-rerunfailures==11.1 +PyYAML==6.0 +requests==2.28.1 +click==8.1.3 +urllib3==1.26.12 +python-jenkins==1.7.0 +loguru==0.6.0 +ddddocr==1.4.7 +dingtalkchatbot==1.5.7 +pydantic==1.10.5 +jsonpath~=0.82 \ No newline at end of file diff --git a/run.py b/run.py new file mode 100644 index 0000000..5b01116 --- /dev/null +++ b/run.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import os +import pytest +import click + + +@click.command() +@click.option('--mark', '-m', default='', help='传入被标记的case套件, 例: -m login') +def run(mark): + pytest.main(['test_cases', f'-m={mark}', '--clean-alluredir', '--alluredir=allure-results']) + os.system("allure generate -c -o allure-report") + + +if __name__ == '__main__': + print(""" + 开始执行项目... + """) + run() diff --git a/send_ding.py b/send_ding.py new file mode 100644 index 0000000..ebad651 --- /dev/null +++ b/send_ding.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import json +import time +import datetime +from utils import config +from utils.path import root + +import click +import jenkins +import urllib3 +from utils.fake_data_control import Execute +from utils.read_yaml_control import HandleYaml +from dingtalkchatbot.chatbot import DingtalkChatbot + +yaml_data = HandleYaml(root / 'config.yml').get_yaml_data() +url = config.jenkins.url +user = config.jenkins.user +password = config.jenkins.pwd +project_name = config.jenkins.project +mapping_url = config.jenkins.mapping_url +robot_webhook = config.ding_talk.webhook + + +class JenkinsContest: + + def __init__(self): + urllib3.disable_warnings() + # jenkins的IP地址 + self.jenkins_url = mapping_url + # jenkins用户名和密码 + self.server = jenkins.Jenkins(self.jenkins_url, username=user, password=password) + + def jenkins_content_info(self): + result_job = self.server.get_jobs() + # jobs_name = result_job[0]["name"] + job_name = project_name + job_url = self.server.get_job_info(job_name)['url'].replace(url, mapping_url) + job_last_number = self.server.get_job_info(job_name)['lastBuild']['number'] + job_result = self.server.get_build_info(job_name, job_last_number)['result'] + report_url = job_url + str(job_last_number) + '/allure' + return result_job, job_name, job_url, job_last_number, report_url, job_result + + +class SendDingTalk(JenkinsContest): + + def __init__(self): + super().__init__() + self.result_job, self.job_name, self.job_url, self.job_last_number, self.report_url, self.job_result = self.jenkins_content_info() + + def send_ding(self): + content = {} + file_path = root / 'allure-report/export/prometheusData.txt' + f = open(file_path) + for line in f.readlines(): + launch_name = line.strip('\n').split(' ')[0] + num = line.strip('\n').split(' ')[1] + content.update({launch_name: num}) + f.close() + passed_num = content['launch_status_passed'] # 通过数量 + failed_num = content['launch_status_failed'] # 失败数量 + broken_num = content['launch_status_broken'] # 阻塞数量 + skipped_num = content['launch_status_skipped'] # 跳过数量 + case_num = content['launch_retries_run'] # 总数量 + run_duration = content['launch_time_duration'] + print(self.job_result) + if self.job_result == 'SUCCESS': + job_result = '成功' + elif self.job_result == 'FAILURE': + job_result = '失败' + elif self.job_result == 'ABORTED': + job_result = '中止' + else: + job_result = '悬挂' + + json_path = root / 'allure-report/widgets/summary.json' + with open(json_path) as f: + res = json.load(f) + today = str(datetime.date.today()) + s = time.localtime(res['time']['start'] / 1000) + report_time = time.strftime("%Y-%m-%d", s) + + if report_time == today: + text = f'### **{self.job_name}接口自动化通知**\n' \ + f"**Jenkins构建结果: {job_result}**\n\n" \ + f"**测试环境: 线上环境**\n\n" \ + f"**时间: {Execute('now_time')()}**\n\n" \ + "------------\n\n" \ + f"### **执行结果**\n\n" \ + f"**成功率: {round(int(passed_num) / int(case_num) * 100)}%**\n\n" \ + f"**总用例数: {case_num}**\n\n" \ + f"**成功用例数: {passed_num}**\n\n" \ + f"**失败用例数: {failed_num}**\n\n" \ + f"**异常用例数: {broken_num}**\n\n" \ + f"**跳过用例数: {skipped_num}**\n\n" \ + f"**本次执行耗时: {round(int(run_duration) / 1000)}秒**\n\n" \ + "------------\n\n" \ + f"**测试报告:** [点击查看]({self.report_url}) \n\n" \ + f"**测试地址:** {yaml_data['host']} \n" + + else: + text = f'### **{self.job_name}自动化通知**\n' \ + f"**Jenkins构建结果: {job_result}**\n\n" \ + "------------\n\n" \ + f"**失败原因: 网络或代理已断开**\n\n" \ + "------------\n\n" \ + f"**构建日志:** [点击查看]({self.job_url + str(self.job_last_number) + '/console'}) \n" + + title = f'接口自动化通知' + send_msg = DingtalkChatbot(webhook=robot_webhook) + send_msg.send_markdown(title=title, + text=text, + is_at_all=True) + + +if __name__ == '__main__': + SendDingTalk().send_ding() + + + diff --git a/test_cases/__init__.py b/test_cases/__init__.py new file mode 100644 index 0000000..3a1213c --- /dev/null +++ b/test_cases/__init__.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + diff --git a/test_cases/__pycache__/__init__.cpython-38.pyc b/test_cases/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..2b2ddfe Binary files /dev/null and b/test_cases/__pycache__/__init__.cpython-38.pyc differ diff --git a/test_cases/__pycache__/conftest.cpython-38-pytest-7.1.2.pyc b/test_cases/__pycache__/conftest.cpython-38-pytest-7.1.2.pyc new file mode 100644 index 0000000..41fe2f5 Binary files /dev/null and b/test_cases/__pycache__/conftest.cpython-38-pytest-7.1.2.pyc differ diff --git a/test_cases/__pycache__/conftest.cpython-38-pytest-7.2.0.pyc b/test_cases/__pycache__/conftest.cpython-38-pytest-7.2.0.pyc new file mode 100644 index 0000000..41fe2f5 Binary files /dev/null and b/test_cases/__pycache__/conftest.cpython-38-pytest-7.2.0.pyc differ diff --git a/test_cases/import/__init__.py b/test_cases/import/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test_cases/import/__pycache__/__init__.cpython-38.pyc b/test_cases/import/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..e7724d8 Binary files /dev/null and b/test_cases/import/__pycache__/__init__.cpython-38.pyc differ diff --git a/test_cases/import/__pycache__/test_1.cpython-38-pytest-7.1.2.pyc b/test_cases/import/__pycache__/test_1.cpython-38-pytest-7.1.2.pyc new file mode 100644 index 0000000..9863b19 Binary files /dev/null and b/test_cases/import/__pycache__/test_1.cpython-38-pytest-7.1.2.pyc differ diff --git a/test_cases/import/__pycache__/test_123.cpython-38-pytest-7.1.2.pyc b/test_cases/import/__pycache__/test_123.cpython-38-pytest-7.1.2.pyc new file mode 100644 index 0000000..bbb13a0 Binary files /dev/null and b/test_cases/import/__pycache__/test_123.cpython-38-pytest-7.1.2.pyc differ diff --git a/test_cases/import/__pycache__/test_123.cpython-38-pytest-7.2.0.pyc b/test_cases/import/__pycache__/test_123.cpython-38-pytest-7.2.0.pyc new file mode 100644 index 0000000..83e9787 Binary files /dev/null and b/test_cases/import/__pycache__/test_123.cpython-38-pytest-7.2.0.pyc differ diff --git a/test_cases/import/__pycache__/test_import.cpython-38-pytest-7.2.0.pyc b/test_cases/import/__pycache__/test_import.cpython-38-pytest-7.2.0.pyc new file mode 100644 index 0000000..7554f8b Binary files /dev/null and b/test_cases/import/__pycache__/test_import.cpython-38-pytest-7.2.0.pyc differ diff --git a/test_cases/import/__pycache__/test_resource1.cpython-38-pytest-7.1.2.pyc b/test_cases/import/__pycache__/test_resource1.cpython-38-pytest-7.1.2.pyc new file mode 100644 index 0000000..f9f6512 Binary files /dev/null and b/test_cases/import/__pycache__/test_resource1.cpython-38-pytest-7.1.2.pyc differ diff --git a/test_cases/import/__pycache__/test_resourcePaperCreate.cpython-38-pytest-7.1.2.pyc b/test_cases/import/__pycache__/test_resourcePaperCreate.cpython-38-pytest-7.1.2.pyc new file mode 100644 index 0000000..c832aff Binary files /dev/null and b/test_cases/import/__pycache__/test_resourcePaperCreate.cpython-38-pytest-7.1.2.pyc differ diff --git a/test_cases/import/__pycache__/test_resourcePaperCreate.cpython-38-pytest-7.2.0.pyc b/test_cases/import/__pycache__/test_resourcePaperCreate.cpython-38-pytest-7.2.0.pyc new file mode 100644 index 0000000..a62eddc Binary files /dev/null and b/test_cases/import/__pycache__/test_resourcePaperCreate.cpython-38-pytest-7.2.0.pyc differ diff --git a/test_cases/import/__pycache__/test_resourcePaperDelete.cpython-38-pytest-7.1.2.pyc b/test_cases/import/__pycache__/test_resourcePaperDelete.cpython-38-pytest-7.1.2.pyc new file mode 100644 index 0000000..471be59 Binary files /dev/null and b/test_cases/import/__pycache__/test_resourcePaperDelete.cpython-38-pytest-7.1.2.pyc differ diff --git a/test_cases/import/__pycache__/test_resourcePaperDelete.cpython-38-pytest-7.2.0.pyc b/test_cases/import/__pycache__/test_resourcePaperDelete.cpython-38-pytest-7.2.0.pyc new file mode 100644 index 0000000..471be59 Binary files /dev/null and b/test_cases/import/__pycache__/test_resourcePaperDelete.cpython-38-pytest-7.2.0.pyc differ diff --git a/test_cases/import/test_import.py b/test_cases/import/test_import.py new file mode 100644 index 0000000..42b8741 --- /dev/null +++ b/test_cases/import/test_import.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import allure +import pytest + + +@allure.feature('XXX模块') +@allure.title('XXX接口') +@pytest.mark.imports +@pytest.mark.datafile('test_data/import/test_import.yml') +def test_import(requests, url, headers, case, inputs, expectation, sql, cache): + sqls = sql.query('select * from uclass.users limit 10') + print(requests, + url, + headers, + case, + inputs, + expectation, + sqls, + cache) + res = requests.post(url, headers=headers, files=inputs['file']).json() + assert res == expectation['response'] + + diff --git a/test_cases/login/__init__.py b/test_cases/login/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test_cases/login/__pycache__/__init__.cpython-38.pyc b/test_cases/login/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..0f0dbd8 Binary files /dev/null and b/test_cases/login/__pycache__/__init__.cpython-38.pyc differ diff --git a/test_cases/login/__pycache__/test_login.cpython-38-pytest-7.1.2.pyc b/test_cases/login/__pycache__/test_login.cpython-38-pytest-7.1.2.pyc new file mode 100644 index 0000000..a5f65f3 Binary files /dev/null and b/test_cases/login/__pycache__/test_login.cpython-38-pytest-7.1.2.pyc differ diff --git a/test_cases/login/__pycache__/test_login.cpython-38-pytest-7.2.0.pyc b/test_cases/login/__pycache__/test_login.cpython-38-pytest-7.2.0.pyc new file mode 100644 index 0000000..a5f65f3 Binary files /dev/null and b/test_cases/login/__pycache__/test_login.cpython-38-pytest-7.2.0.pyc differ diff --git a/test_cases/login/test_login.py b/test_cases/login/test_login.py new file mode 100644 index 0000000..d01c942 --- /dev/null +++ b/test_cases/login/test_login.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import allure +import pytest + + +@allure.feature('登录模块') +@allure.title('登录接口') +@pytest.mark.login +@pytest.mark.datafile('test_data/login/test_login.yml') +def test_login(requests, url, headers, case, inputs, expectation): + res = requests.post(url, json=inputs['json'], headers=headers).json() + assert res == expectation['response'] diff --git a/test_data/import/test_import.yml b/test_data/import/test_import.yml new file mode 100644 index 0000000..4ffacd4 --- /dev/null +++ b/test_data/import/test_import.yml @@ -0,0 +1,23 @@ +common_inputs: + method: POST + path: xxx/import + +tests: + - case: 更换照片 + inputs: + params: {} + json: {"key": "{{random_phone}}"} + file: "import.xlsx" + sql: + expectation: + response: {"code": 0} + + - case: 更换照片2 + inputs: + params: {} + json: {"key": "{{random_num}}"} + file: "import.xlsx" + sql: + expectation: + response: {"code": 0} + diff --git a/test_data/login/test_login.yml b/test_data/login/test_login.yml new file mode 100644 index 0000000..08e705d --- /dev/null +++ b/test_data/login/test_login.yml @@ -0,0 +1,23 @@ +common_inputs: + method: POST + path: "xxx/login" + +tests: + - case: 正确账号、密码登录 + inputs: + params: {} + json: {"account": "{{random_phone}}", "password": "xxxxxxxxxx"} + file: + sql: + expectation: + db_data: + response: {'code': 12, 'msg': '手机号或密码错误'} + + - case: 错误账号、密码登录 + inputs: + params: {} + json: {"account": "18900001000", "password": "xxxxxx"} + expectation: + db_data: + response: + code: 0 \ No newline at end of file diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..77cc09b --- /dev/null +++ b/utils/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +from utils.path import root +from utils.models import Config +from utils.read_yaml_control import HandleYaml + +config_data = HandleYaml(root / 'config.yml').get_yaml_data() +config = Config(**config_data) \ No newline at end of file diff --git a/utils/__pycache__/__init__.cpython-38.pyc b/utils/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..9eef169 Binary files /dev/null and b/utils/__pycache__/__init__.cpython-38.pyc differ diff --git a/utils/__pycache__/allure_control.cpython-38.pyc b/utils/__pycache__/allure_control.cpython-38.pyc new file mode 100644 index 0000000..97622af Binary files /dev/null and b/utils/__pycache__/allure_control.cpython-38.pyc differ diff --git a/utils/__pycache__/database_control.cpython-38.pyc b/utils/__pycache__/database_control.cpython-38.pyc new file mode 100644 index 0000000..eb5ab46 Binary files /dev/null and b/utils/__pycache__/database_control.cpython-38.pyc differ diff --git a/utils/__pycache__/decorator_control.cpython-38.pyc b/utils/__pycache__/decorator_control.cpython-38.pyc new file mode 100644 index 0000000..f2bfa5a Binary files /dev/null and b/utils/__pycache__/decorator_control.cpython-38.pyc differ diff --git a/utils/__pycache__/fake_data_control.cpython-38.pyc b/utils/__pycache__/fake_data_control.cpython-38.pyc new file mode 100644 index 0000000..e00254a Binary files /dev/null and b/utils/__pycache__/fake_data_control.cpython-38.pyc differ diff --git a/utils/__pycache__/get_authentication_control.cpython-38.pyc b/utils/__pycache__/get_authentication_control.cpython-38.pyc new file mode 100644 index 0000000..10d6812 Binary files /dev/null and b/utils/__pycache__/get_authentication_control.cpython-38.pyc differ diff --git a/utils/__pycache__/models.cpython-38.pyc b/utils/__pycache__/models.cpython-38.pyc new file mode 100644 index 0000000..0306821 Binary files /dev/null and b/utils/__pycache__/models.cpython-38.pyc differ diff --git a/utils/__pycache__/open_file_control.cpython-38.pyc b/utils/__pycache__/open_file_control.cpython-38.pyc new file mode 100644 index 0000000..2de1002 Binary files /dev/null and b/utils/__pycache__/open_file_control.cpython-38.pyc differ diff --git a/utils/__pycache__/path.cpython-38.pyc b/utils/__pycache__/path.cpython-38.pyc new file mode 100644 index 0000000..eb90c44 Binary files /dev/null and b/utils/__pycache__/path.cpython-38.pyc differ diff --git a/utils/__pycache__/read_yaml_control.cpython-38.pyc b/utils/__pycache__/read_yaml_control.cpython-38.pyc new file mode 100644 index 0000000..6bb4ae0 Binary files /dev/null and b/utils/__pycache__/read_yaml_control.cpython-38.pyc differ diff --git a/utils/__pycache__/requests_control.cpython-38.pyc b/utils/__pycache__/requests_control.cpython-38.pyc new file mode 100644 index 0000000..9e6a6ef Binary files /dev/null and b/utils/__pycache__/requests_control.cpython-38.pyc differ diff --git a/utils/allure_control.py b/utils/allure_control.py new file mode 100644 index 0000000..7b62668 --- /dev/null +++ b/utils/allure_control.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import json +import allure +from typing import Any, Dict, Union, Optional + + +class ReportStyle: + """allure 报告样式""" + + @staticmethod + def allure_step(step: str, var: Optional[Union[str, Dict[str, Any]]] = None): + with allure.step(step): + allure.attach( + json.dumps(var, ensure_ascii=False, indent=4), + step, + allure.attachment_type.JSON, + ) + + @staticmethod + def title(title: str): + allure.dynamic.title(title) + + @staticmethod + def allure_step_no(step: str): + """ + 无附件的操作步骤 + :param step: 步骤名称 + :return: + """ + with allure.step(step): + pass + diff --git a/utils/database_control.py b/utils/database_control.py new file mode 100644 index 0000000..a5f74db --- /dev/null +++ b/utils/database_control.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import pymysql +from utils import config +from loguru import logger + + +class MysqlDB: + """ 数据库 封装 """ + + def __init__(self): + + try: + # 建立数据库连接 + self.conn = pymysql.connect( + host=config.mysql_db.host, + user=config.mysql_db.user, + password=config.mysql_db.password, + port=config.mysql_db.port, + ) + + # 使用 cursor 方法获取操作游标,得到一个可以执行sql语句,并且操作结果为字典返回的游标 + self.cur = self.conn.cursor(cursor=pymysql.cursors.DictCursor) + except AttributeError as error: + logger.error("数据库连接失败,失败原因 %s", error) + + def __del__(self): + try: + # 关闭游标 + self.cur.close() + # 关闭连接 + self.conn.close() + except AttributeError as error: + logger.error("数据库连接失败,失败原因 %s", error) + + def query(self, sql, state="all"): + """ + 查询 + :param sql: + :param state: all 是默认查询全部 + :return: + """ + try: + self.cur.execute(sql) + + if state == "all": + # 查询全部 + data = self.cur.fetchall() + else: + # 查询单条 + data = self.cur.fetchone() + return data + except AttributeError as error_data: + logger.error("数据库连接失败,失败原因 %s", error_data) + raise + + def execute(self, sql): + """ + 更新 、 删除、 新增 + :param sql: + :return: + """ + try: + # 使用 execute 操作 sql + rows = self.cur.execute(sql) + # 提交事务 + self.conn.commit() + return rows + except AttributeError as error: + logger.error("数据库连接失败,失败原因 %s", error) + # 如果事务异常,则回滚数据 + self.conn.rollback() + raise + diff --git a/utils/decorator_control.py b/utils/decorator_control.py new file mode 100644 index 0000000..5a3e529 --- /dev/null +++ b/utils/decorator_control.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import json +from loguru import logger +from utils.allure_control import ReportStyle + + +class Log: + """日志操作装饰器""" + + def __init__(self, switch=True): + self._switch = switch + + def __call__(self, func): + def wrapper(*args, **kwargs): + if self._switch is True: + res = func(*args, **kwargs) + _url = args[1] if args[1] else {} + _method = args[2] if args[2] else {} + if _method == 'post': + _headers = args[5] if args[5] else {} + _data = args[3] if args[3] else {} + _json = args[4] if args[4] else {} + elif _method == 'get': + _headers = args[3] if args[3] else {} + _data = kwargs if kwargs else {} + _json = kwargs if kwargs else {} + r = res.text + try: + resp = json.loads(r) + except json.decoder.JSONDecodeError: + logger.info(f'请求返回结果为text') + resp = res.status_code + logger.info("\n===============================================================") + res_info = f"请求地址: {_url}\n" \ + f"请求方法: {_method.upper()}\n" \ + f"请求头: {_headers}\n" \ + f"请求数据: {_data if _data else _json}\n\n" \ + f"响应数据: {resp}\n" \ + f"响应耗时(ms): {float(round(res.elapsed.total_seconds() * 1000))}\n" \ + f"接口响应码: {res.status_code}" + logger.info(res_info) + ReportStyle.allure_step_no(f"请求地址: {_url}") + ReportStyle.allure_step_no(f"请求方法: {_method.upper()}") + ReportStyle.allure_step("请求头", _headers) + ReportStyle.allure_step("请求数据", _data if _data else _json) + ReportStyle.allure_step_no(f"接口响应码: {res.status_code}") + ReportStyle.allure_step_no(f"响应耗时(ms): {round(res.elapsed.total_seconds() * 1000)}") + ReportStyle.allure_step("响应数据", resp) + return res + return wrapper diff --git a/utils/fake_data_control.py b/utils/fake_data_control.py new file mode 100644 index 0000000..baef417 --- /dev/null +++ b/utils/fake_data_control.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import inspect +import random +from faker import Faker +from datetime import datetime + + +class Execute: + """ Mock数据 """ + + def __init__(self, func_name): + self.faker = Faker(locale='zh_CN') + self.func_name = func_name + self.func_map = self.func_list() + + def __call__(self, *args, **kwargs): + func = self.func_map.get(self.func_name, None) + if func is not None and callable(func): + return func() + else: + print("未获取到该字段对应方法,请检查") + + def func_list(self): + """ + :return: 返回除内置方法外类中的所有其他方法 + """ + func_list = {} + all_method = inspect.getmembers(self, inspect.ismethod) + for name in all_method: + func_list[name[0]] = eval(f'self.{name[0]}') if '__' not in str(name[0]) else ... + return func_list + + @classmethod + def random_int(cls): + """ + :return: 随机数 + """ + _data = random.randint(0, 5000) + return _data + + def random_phone(self): + """ + :return: 随机生成手机号码 + """ + return '1140124' + str(self.faker.phone_number()[7:]) + + def random_id_number(self): + """ + + :return: 随机生成身份证号码 + """ + + id_number = self.faker.ssn() + return id_number + + def random_female_name(self): + """ + + :return: 女生姓名 + """ + female_name = self.faker.name_female() + return female_name + + def random_male_name(self): + """ + + :return: 男生姓名 + """ + male_name = self.faker.name_male() + return male_name + + def random_email(self): + """ + + :return: 生成邮箱 + """ + email = self.faker.email() + return email + + @classmethod + def now_time(cls): + """ + 计算当前时间 + :return: + """ + now_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + return now_time + + @classmethod + def now_time_stamp(cls): + """ + 计算当前时间戳(秒级) + :return: + """ + now_time_stamp = datetime.now().timestamp() + return int(now_time_stamp) + + def random_num(self): + """ + + :return: 随机1~10数字 + """ + num = random.randint(1, 10) + return num + + +if __name__ == '__main__': + r = Execute('random_num')() + print(r) diff --git a/utils/get_authentication_control.py b/utils/get_authentication_control.py new file mode 100644 index 0000000..79dc242 --- /dev/null +++ b/utils/get_authentication_control.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from utils.path import root +from jsonpath import jsonpath +from utils.read_yaml_control import HandleYaml + + +class Authentication: + """获取token/cookies""" + + def __init__(self): + self.handle_yaml = HandleYaml(root / 'config.yml') + self.payload = {"account": self.handle_yaml.get_yaml_data()['account'], + "password": self.handle_yaml.get_yaml_data()['password'], + "isVaildCode": False} + + @property + def cookie_token(self): + import requests + res = requests.post('https://xxxxxxx/login', data=self.payload) + res_cookies, res_token = res.cookies, jsonpath(res.json(), '$..token')[0] + return res_cookies, res_token + + +cookies, token = Authentication().cookie_token diff --git a/utils/models.py b/utils/models.py new file mode 100644 index 0000000..4389198 --- /dev/null +++ b/utils/models.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from typing import Union +from pydantic import BaseModel + + +class Jenkins(BaseModel): + url: Union[str, None] + mapping_url: Union[str, None] + user: Union[str, int, None] + pwd: Union[str, int, None] + project: Union[str, None] + + +class DingTalk(BaseModel): + webhook: Union[str, None] + + +class MySqlDB(BaseModel): + host: Union[str, None] + user: Union[str, None] + password: Union[str, None] + port: Union[int, None] + + +class Config(BaseModel): + project_name: Union[str, None] + env: Union[str, None] + tester_name: Union[str, None] + host: Union[str, None] + account: Union[str, int, None] + password: Union[str, int, None] + jenkins: "Jenkins" + ding_talk: "DingTalk" + mysql_db: "MySqlDB" + + diff --git a/utils/open_file_control.py b/utils/open_file_control.py new file mode 100644 index 0000000..b63204c --- /dev/null +++ b/utils/open_file_control.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from utils.path import root + + +def open_file(file): + """封装接口请求打开文件 函数""" + + file_path = str(root / f'files/{file}') + file_name = file_path.split('/')[-1] + return {'file': (file_name, open(file_path, 'rb'), 'application/json')} + + +if __name__ == '__main__': + r = open_file('avatar_new.png') + print(r) \ No newline at end of file diff --git a/utils/path.py b/utils/path.py new file mode 100644 index 0000000..9c70efe --- /dev/null +++ b/utils/path.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from pathlib import Path + +root = Path(__file__).resolve().parents[1] diff --git a/utils/read_yaml_control.py b/utils/read_yaml_control.py new file mode 100644 index 0000000..91f3e12 --- /dev/null +++ b/utils/read_yaml_control.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import os +import yaml + + +class HandleYaml: + + def __init__(self, file_path): + self._file_path = file_path + + def get_yaml_data(self): + """读取yaml文件数据""" + + if os.path.exists(self._file_path): + with open(self._file_path, 'r', encoding='utf-8') as data: + res = yaml.load(data, Loader=yaml.FullLoader) + else: + raise FileNotFoundError("文件路径不存在,请检查") + return res + + +if __name__ == '__main__': + pass + + diff --git a/utils/requests_control.py b/utils/requests_control.py new file mode 100644 index 0000000..fa26329 --- /dev/null +++ b/utils/requests_control.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import urllib3 +import requests +from utils.decorator_control import Log +from utils.get_authentication_control import Authentication + +cookie, token = Authentication().cookie_token + + +class RestClient: + """封装api请求类""" + + def __init__(self): + urllib3.disable_warnings() + self.sesson = requests.session() # 创建会话对象 + self.sesson.cookies = cookie + + def get(self, url, headers=None, **kwargs): + return self.request(url, "get", headers, verify=False, **kwargs) + + def post(self, url, data=None, json=None, headers=None, **kwargs): + return self.request(url, "post", data, json, headers, verify=False, **kwargs) + + def options(self, url, **kwargs): + return self.request(url, "options", verify=False, **kwargs) + + def head(self, url, **kwargs): + return self.request(url, "head", **kwargs) + + def put(self, url, data=None, **kwargs): + return self.request(url, "put", data, verify=False, **kwargs) + + def patch(self, url, data=None, json=None, **kwargs): + return self.request(url, "patch", data, json, verify=False, **kwargs) + + def delete(self, url, **kwargs): + return self.request(url, "delete", verify=False, **kwargs) + + @Log(True) + def request(self, url, request_method, data=None, json=None, headers=None, **kwargs): + + if request_method == 'get': + res = self.sesson.get(url, headers=headers, **kwargs) + elif request_method == 'post': + res = self.sesson.post(url, json, data, headers=headers, **kwargs) + elif request_method == 'options': + res = self.sesson.options(url, **kwargs) + elif request_method == 'head': + res = self.sesson.head(url, **kwargs) + elif request_method == 'put': + res = self.sesson.put(url, data, **kwargs) + elif request_method == 'patch': + data = json.dump(json) if json else ... + res = self.sesson.patch(url, data, **kwargs) + elif request_method == 'delete': + res = self.sesson.delete(url, **kwargs) + + return res + + @staticmethod + def headers(): + """全局headers""" + headers = { + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36', + 'ci-token': token + } + return headers + + +if __name__ == '__main__': + pass