最近文章一直都是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
作者:李 鋒|編輯排版:梁莉莉