最近文章一直都是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
作者:李 锋|编辑排版:梁莉莉
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)