python:接口自动化项目测试框架
最近文章一直都是python的第三方库使用及爬虫的知识,针对自动化测试的优化版本也没有及时发布出来,今天主要抽时间整理了一下,罗列了运行流程及项目工程目录。
所提供的框架仅供参考,中间还有很多不足之处,也希望大家踊跃提出疑义和建议。
下面进入代码的世界……
工程目录
apiTest
├─apiInterface
├─cases
├─common
├─config
├─dynamicData
├─logs
├─reports
│ ├─allure
│ └─html
├─runMain
├─testDatas
├─requirements.txt
└─settings.py
- apiTest:根目录
- apiInterface:是存放一些url路径,这里只是返回路径,未做多余操作
- cases:测试用例存放文件夹
- common:存放一些公共调用的类
- config:配置文件存放目录
- dynamicData:动态参数存放处,就是指一些接口的上下游需要的参数
- logs:存放程序运行的日志文件
- reports:存放程序运行完成,生成的测试报告,分为allure和html报告
- runMain:存放程序运行入口的目录
- testDatas:测试数据目录,主要是yaml文件
- requirements.txt:程序需要的依赖包
- settings.py:配置文件,路径,环境切换及各类配置的存放,类似django的setting文件
框架的运行流程
主要是运用了python、request、pytest、yaml、Jinja2、allure
组成的测试框架.
其运行流程就是执行测试用例时,会先拿接口路径,其次再读取yaml文件,然后yaml文件替换需要替换的动态数据,再然后就是接口拿到数据取利用python++pytest+requests库去请求,然后根据返回值进行断言及生成allure报告。
实际代码介绍
apiTest/apiInterface/apiMatch.py
# -*-coding:utf-8 -*-
# ** createDate: 2021/11/16 11:37
# ** scriptFile: apiMatch.py
# ** __author__: Li Feng
"""
注释信息:
"""
__all__ = ["api_match"]
class _ApiMatch:
@property
def match_european_cup(self):
"""
2021欧洲杯赛程
查询2021欧洲杯赛程详细信息
:return:
"""
return "/fapig/euro2020/schedule"
api_match = _ApiMatch()
apiTest/cases/test_european_cup.py
# -*- encoding: utf-8 -*-
"""
@__Author__: lifeng
@__Software__: PyCharm
@__File__: test_european_cup.py
@__Date__: 2021/6/13 19:00
"""
import pytest
from common.readRenderYaml import render
from apiInterface.apiMatch import api_match
from dynamicData.matchDynamic import contents
class TestNews:
# 读取yaml文件,并执行数据替换(contents=contents就是接收需要替换的参数)
data = render("api_match", "test_european_cup", contents=contents)
@pytest.mark.parametrize('test_data, title, results', data["test_data"])
def test_european_cup(self, test_data, title, results, auth):
"""
2021欧洲杯赛程
:param test_data: 测试数据
:param title: 传参名称
:param results: 预期结果
:param auth: 登录后返回一个请求对象
:return:
"""
response = auth.send_get(api_match.match_european_cup, test_data)
assert response["reason"] == results["reason"]
assert type(response["result"]["data"]) == type(results["result"]["data"])
if __name__ == '__main__':
pytest.main(["-v", "-s", "test_european_cup.py"])
apiTest/common/readRenderYaml.py
在cases
目录中的test_european_cup.py
文件中调用的render
函数就是下面的这个类提供的。
它的只要功能就是读取yaml
文件然后执行Jinja2
库进行动态参数替换。
# -*-coding:utf-8 -*-
# ** createDate: 2021/11/16 8:18
# ** scriptFile: readRenderYaml.py
# ** __author__: Li Feng
"""
注释信息:
"""
import json
import yaml
import jinja2
from common.mapMnvironment import MapEnvironment
__all__ = ["render"]
class _ReadYamlRender:
def __init__(self, yaml_path_name: str, yaml_tier: str, contents: dict = None):
self._content = contents
"""
读取yaml文件的数据, 返回正经json数据
"""
with open(yaml_path_name, encoding="utf-8") as y:
data = yaml.safe_load(y)
# json.dumps要把字符串数据转成正经json数据,用于return返回时不报错
self._template_name = json.dumps(data[yaml_tier])
@property
def render(self):
"""
利用jinja2进行动态数据渲染替换,返回字典类型
:return:
"""
if self._content is not None:
jinja2_data = jinja2.Template(self._template_name).render(self._content)
return json.loads(jinja2_data)
else:
return json.loads(self._template_name)
def render(path_key: str, yaml_tier: str, contents: dict = None):
"""
执行读取yaml文件并渲染返回数据
:param path_key:
:param yaml_tier:
:param content:
:return:
"""
# 获取所有yaml文件路径
data = MapEnvironment().yaml_path
# 渲染yaml文件
return _ReadYamlRender(data[path_key], yaml_tier, contents).render
apiTest/common/sendRequest.py
在cases
目录中的test_european_cup.py
文件中调用的auth.send_get
方法就是下面的这个请求类提供的。
它的只要功能就是进行接口的请求,可能你会疑问为什么是auth。seng_get
,那是因为我这里用了pytest框架提供的测试夹具功能(这个后面会单独说pytest框架,在本篇文章了解下即可)。
import json
import urllib3
import requests
from functools import wraps
from requests import exceptions
from requests_toolbelt import MultipartEncoder
from common.logLogging import do_logger
from common.mapMnvironment import MapEnvironment
__all__ = ["send"]
def _handle_response(func):
"""
处理请求后的返回值
:param func: 传入函数
:return:
"""
@wraps(func)
def wraps_response(*args, **kwargs):
results = func(*args, **kwargs)
request_body = results.request.body
request_url = results.request.url
try:
if results.ok:
return results.json()
except json.JSONDecodeError:
return results.text.encode("utf-8")
except Exception as _error:
do_logger.error(f"接口请求出错:"
f"请求url:{request_url},"
f"请求参数:{request_body},"
f"返回数据:{results.text}")
raise exceptions.RequestException from _error
return wraps_response
def _print_url(r, *args, **kwargs):
"""
回调函数,r接受一个数据块作为它的第一个参数
:param r:
:param args:
:param kwargs:
:return:
"""
print(f"请求url:{r.request.url}")
print(f"请求参数:{r.request.body}")
# print(f"请求数据:{r.request.prepare()}")
print(f"返回数据:{r.text}")
class _SendRequest:
_map = MapEnvironment()
def __init__(self):
urllib3.disable_warnings()
self.s = requests.Session()
self.s.verify = False
self.headers = self.s.headers
self.headers.update(MapEnvironment().headers)
urllib3.disable_warnings(urllib3.exceptions.InsecurePlatformWarning)
@classmethod
def _get_url(cls, url):
"""
拼接url,增加platform参数
:param url:
:return:
"""
return cls._map.base_url(cls._map.host) + url
def send_upload(self, url, filename, filetype='application/vnd.ms-excel'):
"""
上传文件请求
:param url:
:param filename: 文件名称
:param filetype: 文件类型
:return:
"""
try:
url = self._get_url(url)
with self.s as interface:
from pathlib import Path
m = MultipartEncoder(
fields={'file': (
filename, open(Path().parent.joinpath("upload"), 'rb'), filetype)})
self.s.headers.update({"Content-Type": m.content_type})
response = interface.post(url=url, data=m, hooks=dict(response=_print_url))
results = json.loads(json.dumps(response.text))
except Exception as e:
do_logger.error(e)
raise (ImportError, FileNotFoundError, PermissionError) from e
else:
return results
def send_download(self, url, filename, params=None, **kwargs):
"""
下载文件请求
:param url:
:param filename: 文件的名称加后缀名(例:name.xlsx)
:param params:
:param kwargs:
:return:
"""
url = self._get_url(url)
with self.s as interface:
response = interface.get(url=url, params=params,
hooks=dict(response=_print_url), **kwargs)
try:
from pathlib import Path, PurePath
if response.ok:
with open(PurePath(Path(__file__).parent).parent.joinpath("download", filename), 'wb') as save:
for chunk in response.iter_content():
save.write(chunk)
except Exception as e:
do_logger.error(e)
raise (ImportError, FileNotFoundError, PermissionError) from e
else:
return True
@_handle_response
def send_get(self, url, params=None, **kwargs):
"""
get请求
:param url:
:param params:
:param kwargs: 动态参数
:return: 返回状态码
"""
url = self._get_url(url)
with self.s as interface:
response = interface.get(url, params=params,
hooks=dict(response=_print_url), **kwargs)
return response
@_handle_response
def send_post(self, url, json=None, data=None, query=None, **kwargs):
"""
post请求
:param url:
:param json:
:param data:
:param query: 接收url跟随的参数
:param kwargs: 动态参数
:return: 返回状态码
"""
url = self._get_url(url)
with self.s as interface:
response = interface.post(url=url, data=data,
json=json, params=query,
hooks=dict(response=_print_url), **kwargs)
return response
@_handle_response
def send_put(self, url, json=None, data=None, query=None, **kwargs):
"""
put请求
:param url:
:param json:
:param data:
:param query: 接收url跟随的参数
:param kwargs: 动态参数
:return: 返回状态码
"""
url = self._get_url(url)
with self.s as interface:
response = interface.put(url=url, data=data,
json=json, params=query,
hooks=dict(response=_print_url), **kwargs)
return response
@_handle_response
def send_delete(self, url, **kwargs):
"""
delete请求
:param url:
:param kwargs: 动态参数
:return: 返回状态码
"""
url = self._get_url(url)
with self.s as interface:
response = interface.delete(url=url,
hooks=dict(response=_print_url), **kwargs)
return response
# 创建对象
send = _SendRequest()
apiTest/dynamicData/matchDynamic.py
在cases
目录中的test_european_cup.py
文件中可以看到render(xx, xx, contents=contents)
,它就是把需要替换的动态参数传给Jinja2
去执行替换操作。
# -*-coding:utf-8 -*-
# ** createDate: 2021/11/16 11:40
# ** scriptFile: matchDynamic.py
# ** __author__: Li Feng
"""
注释信息:
"""
__document__ = """
存放一些动态数据,用于yaml文件中的数据替换操作
"""
contents = {
"key": "9d0dfd9dbaf51de283ee8a88e58e332b"
}
apiTest/logs
存放程序运行时,出现错误的日志。
apiTest/reports
存放allure和html报告目录,allure生成的是json文件,所有尽量再建一个子文件夹。
apiTest/runMain/main.py
这里就是执行pytest,运行全部用例,然后生成allure报告和html报告,并存放在reports
目录中。
# -*-coding:utf-8 -*-
# ** createDate: 2021/11/16 11:31
# ** scriptFile: main.py
# ** __author__: Li Feng
"""
注释信息:
"""
import os
import sys
import pytest
# 执行路径插入操作,增强代码的可移植性
sys.path.insert(0, os.path.dirname(os.path.dirname(os.getcwd())))
print(sys.path)
def main():
# 入口函数,运行全部用例,生成html和allure报告
pytest.main(['../cases/', '--html=../reports/report.html',
'--alluredir=../reports/allure/allure-report'])
if __name__ == '__main__':
main()
apiTest/testDatas
存放yaml文件,yaml文件中主要是放一些测试数据,针对动态的测试数据,要根据Jinja2
的语法来使用:
test_european_cup:
test_data:
# 接口参数
- - type: 1
key: "{{key}}"
# 接口传参名称
- name: type字段传1
# 实际结果,用于断言操作
- reason: "查询成功!"
result:
data:
-
- - type: 2
key: "{{key}}"
# 接口传参名称
- name: type字段传2
# 实际结果,用于断言操作
- reason: "查询成功!"
result:
data:
-
- - type: 3
key: "{{key}}"
# 接口传参名称
- name: type字段传3
# 实际结果,用于断言操作
- reason: "查询成功!"
result:
data:
-
{{key}}
这里的意思就是取key
的value
值,而value
值就是apiTest/dynamicData/matchDynamic.py
文件中提供的:
# -*-coding:utf-8 -*-
# ** createDate: 2021/11/16 11:40
# ** scriptFile: matchDynamic.py
# ** __author__: Li Feng
"""
注释信息:
"""
__document__ = """
存放一些动态数据,用于yaml文件中的数据替换操作
"""
contents = {
"key": "9d0dfd9dbaf51de283ee8a88e58e218b"
}
apiTest/requirements.txt
就是你用的一些依赖包
allure-pytest
allure-python-commons
Appium-Python-Client
beautifulsoup4
jmespath
jsonpath
jsonschema
mysqlclient==1.4.6
openpyxl
pyaml
PyMySQL
pytest
pytest-base-url
pytest-cov
pytest-cover
pytest-emoji
pytest-html
pytest-metadata
pytest-rerunfailures
pytest-xdist
python-jenkins
PyYAML
redis
requests
requests-toolbelt
selenium
pytest-pikachu
pytest-clarity
apiTest/settings.py
它主要就是一个配置文件,配置账号密码,日志,环境变量等:
# -*-coding:utf-8 -*-
# ** createDate: 2021/11/16 8:12
# ** scriptFile: setting.py
# ** __author__: Li Feng
"""
注释信息:
"""
from pathlib import Path, PurePath
# 获取项目根目录
BASE_DIR = PurePath(Path(__file__).parent)
print(BASE_DIR)
# 用于判断是否往企业微信发送测试报告:True是发送、False是不发送
IS_SEND = True
# 设置运行的环境变量
ENVIRONMENT = "PRO" # 环境变量值分别为 测试:TEST;预发布:PRE;生产:PRO
# 接口请求域名
HOST = "http://apis.juhe.cn"
# 设置头信息指定域名和Content-Type类型
HEADERS = {'Content-Type': 'application/json'}
# 环境IP配置
BASE_HOST = {
"test": None,
"pre": None,
"pro": None,
}
# 数据库配置
DATABASES = {
"pro": {"host": "8.136.250.157", "port": 1234, "user": "root", "passwd": "test.2016", "db": "testing"},
}
# yaml文件路径
YAML_FILE_PATH = {
"api_idiom": BASE_DIR.joinpath("testDatas", "idiom_modules.yml"),
"api_match": BASE_DIR.joinpath("testDatas", "match_modules.yml"),
}
# 日志存放目录
LOGGING_PATH = BASE_DIR.joinpath("logs", f"logfile.text")
# 日志记录配置
LOGGING_CONFIG = {
"version": 1,
"root": {
"level": "DEBUG",
"handlers": ["file", "console"]
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": "ERROR",
"formatter": "console_formatters"
},
"file": {
"class": "logging.handlers.RotatingFileHandler",
"formatter": "file_formatters",
"filename": LOGGING_PATH,
"level": "DEBUG",
"maxBytes": 100,
"backupCount": 5,
"encoding": "utf-8"
}
},
"formatters": {
"console_formatters": {
"format": "%(asctime)s [%(name)s:%(lineno)d] [%(module)s:%(funcName)s] [%(levelname)s]- %(message)s",
"datefmt": "%Y%m%d %H:%M:%S"
},
"file_formatters": {
'format': "%(asctime)s [%(name)s:%(lineno)d] [%(module)s:%(funcName)s] [%(levelname)s]- %(message)s- %(pathname)s",
"datefmt": "%Y%m%d %H:%M:%S"
}
}
}
从配置文件中可以清晰看到日志的配置、环境的配置、还有发送邮件的配置等,这些都需要公共方法调用的,后续会给补充上来,以上就是近期对项目框架的一些优化,后续会把pytest框架的使用及对应的三方库整理出来,allure报告的使用也会整理出来,会做成一套接口自动化教程。
可能中间还有很多不好之处,也会慢慢改善进行优化,一个阶段的努力也是一个阶段的提升,总结这个阶段的结果,是我们追求的星辰大海,哪怕它很慢。
以上总结或许能帮助到你,或许帮助不到你,但还是希望能帮助到你,如有疑问、歧义,直接私信留言会及时修正发布;非常期待你的点赞和分享哟,谢谢!
未完,待续…
一直都在努力,希望您也是!
微信搜索公众号:就用python
作者:李 锋|编辑排版:梁莉莉