yamlpy接口测试框架
1、思路:
yamlpy即为yaml文件+pytest单元测试框架的缩写,
可以看作是一个脚手架工具,
可以快速生成项目的各个目录与文件,
只需维护一份或者多份yaml文件即可,
不需要大量写代码。
与yamlapi接口测试框架对比,
整体结构仍然保持不变,
yaml文件格式仍然保持不变,
可以通用,
抛弃了python自带的unittest单元测试框架、ddt数据驱动第三方库、BeautifulReport测试报告第三方库,
修改了测试类文件,
传参方式由ddt的@ddt.ddt、@ddt.file_data()改为pytest的@pytest.mark.parametrize(),
删掉了tool工具包里面的beautiful_report_run.py文件,
其他文件保持不变,
新增了pytest-assume多重断言插件。
(yamlapi接口测试框架也支持双重断言)
2、安装:
pip install yamlpy
请访问
https://pypi.org/project/yamlpy/
3、文件举例:
README.md文件:
# yamlpy
接口测试框架 # 一、思路
1、采用requests+PyMySQL+demjson+loguru+PyYAML+ruamel.yaml+pytest+pytest-html+allure-pytest+pytest-assume+pytest-rerunfailures+pytest-sugar+pytest-timeout
2、requests是发起HTTP请求的第三方库
3、PyMySQL是连接MySQL的第三方库
4、demjson是解析json的第三方库
5、loguru是记录日志的第三方库
6、PyYAML与ruamel.yaml是读写yaml文件的第三方库
7、pytest是单元测试的第三方库
8、pytest-html是生成html测试报告的插件
9、allure-pytest是生成allure测试报告的插件
10、pytest-assume是多重断言的插件
11、pytest-rerunfailures是失败重跑的插件
12、pytest-sugar是显示进度的插件
13、pytest-timeout是设置超时时间的插件 # 二、目录结构
1、case是测试用例包
2、log是日志目录
3、report是测试报告的目录
4、resource是yaml文件的目录
5、setting是工程的配置文件包
6、tool是常用方法的封装包 # 三、yaml文件说明
1、字段(命名和格式不可修改,顺序可以修改)
case_name: 用例名称
mysql: MySQL语句,-列表格式,顺序不可修改
第一行:mysql[0]
第二行:mysql[1]
第三行:mysql[2]
第一行为增删改语句,第二行为查语句,第三行为查语句(数据库双重断言)
第一行是发起请求之前的动作,没有返回结果
第二行是发起请求之前的动作,有返回结果,是为了动态传参
第三行是发起请求之后的动作,有返回结果,但是不可用于动态传参,是为了断言实际的响应结果
当不需要增删改查和双重断言时,三行都为空
当只需要增删改时,第一行为增删改语句,第二行为空,第三行为空
当只需要查时,第一行为空,第二行为查语句,第三行为空
当只需要双重断言时,第一行为空,第二行为空,第三行为查语句
request_mode: 请求方式
api: 接口路径
data: 请求体,缩进字典格式或者json格式
headers: 请求头,缩进字典格式或者json格式
query_string: 请求参数,缩进字典格式或者json格式
expected_code: 预期的响应代码
expected_result: 预期的响应结果,-列表格式、缩进字典格式或者json格式
regular: 正则,缩进字典格式
>>variable:变量名,-列表格式
>>expression:表达式,-列表格式 2、参数化
正则表达式提取的结果用${变量名}匹配,一条用例里面可以有多个
MySQL查询语句返回的结果,即第二行mysql[1]返回的结果,用{__SQL索引}匹配
即{__SQL0}、{__SQL1}、{__SQL2}、{__SQL3}。。。。。。一条用例里面可以有多个
随机数字用{__RN位数},一条用例里面可以有多个
随机英文字母用{__RL位数},一条用例里面可以有多个
以上4种类型在一条用例里面可以混合使用
${变量名}的作用域是全局的,其它3种的作用域仅限该条用例 # 四、运行
在工程的根目录下执行命令
pytest+--cmd=环境缩写
pytest --cmd=dev
pytest --cmd=test
pytest --cmd=pre
pytest --cmd=formal
demo_test.py文件:
"""
测试用例
""" import json
import re
from itertools import chain
from time import sleep import allure
import demjson
import pytest
import requests
from pytest_assume.plugin import assume
from setting.project_config import *
from tool.connect_mysql import ConnectMySQL
from tool.read_write_yaml import merge_yaml
from tool.function_assistant import function_dollar, function_rn, function_rl, function_sql @allure.feature(test_scenario)
class DemoTest(object):
temporary_list = merge_yaml() # 调用合并所有yaml文件的方法 @classmethod
def setup_class(cls):
cls.variable_result_dict = {}
# 定义一个变量名与提取的结果字典
# cls.variable_result_dict与self.variable_result_dict都是本类的公共属性 @allure.story(test_story)
@allure.severity(test_case_priority[0])
@allure.testcase(test_case_address, test_case_address_title)
@pytest.mark.parametrize("temporary_dict", temporary_list)
# 传入临时列表
def test_demo(self, temporary_dict):
"""
测试用例
:param temporary_dict:
:return:
""" global mysql_result_list_after temporary_dict = str(temporary_dict)
if "None" in temporary_dict:
temporary_dict = temporary_dict.replace("None", "''")
temporary_dict = demjson.decode(temporary_dict)
# 把值为None的替换成''空字符串,因为None无法拼接
# demjson.decode()等价于json.loads()反序列化 case_name = temporary_dict.get("case_name")
# 用例名称
self.test_order.__func__.__doc__ = case_name
# 测试报告里面的用例描述
mysql = temporary_dict.get("mysql")
# mysql语句
request_mode = temporary_dict.get("request_mode")
# 请求方式
api = temporary_dict.get("api")
# 接口路径
if type(api) != str:
api = str(api)
payload = temporary_dict.get("data")
# 请求体
if type(payload) != str:
payload = str(payload)
headers = temporary_dict.get("headers")
# 请求头
if type(headers) != str:
headers = str(headers)
query_string = temporary_dict.get("query_string")
# 请求参数
if type(query_string) != str:
query_string = str(query_string)
expected_code = temporary_dict.get("expected_code")
# 预期的响应代码
expected_result = temporary_dict.get("expected_result")
# 预期的响应结果
if type(expected_result) != str:
expected_result = str(expected_result)
regular = temporary_dict.get("regular")
# 正则 logger.info("{}>>>开始执行", case_name)
if environment == "formal" and mysql:
pytest.skip("生产环境跳过此用例,请忽略")
# 生产环境不能连接MySQL数据库,因此跳过 if self.variable_result_dict:
# 如果变量名与提取的结果字典不为空
if mysql:
if mysql[0]:
mysql[0] = function_dollar(mysql[0], self.variable_result_dict.items())
# 调用替换$的方法
if mysql[1]:
mysql[1] = function_dollar(mysql[1], self.variable_result_dict.items())
if mysql[2]:
mysql[2] = function_dollar(mysql[2], self.variable_result_dict.items())
if api:
api = function_dollar(api, self.variable_result_dict.items())
if payload:
payload = function_dollar(payload, self.variable_result_dict.items())
if headers:
headers = function_dollar(headers, self.variable_result_dict.items())
if query_string:
query_string = function_dollar(query_string, self.variable_result_dict.items())
if expected_result:
expected_result = function_dollar(expected_result, self.variable_result_dict.items())
else:
pass if mysql:
db = ConnectMySQL()
# 实例化一个MySQL操作对象
if mysql[0]:
mysql[0] = function_rn(mysql[0])
# 调用替换RN随机数字的方法
mysql[0] = function_rl(mysql[0])
# 调用替换RL随机字母的方法
if "INSERT" in mysql[0]:
db.insert_mysql(mysql[0])
# 调用插入mysql的方法
sleep(2)
# 等待2秒钟
if "UPDATE" in mysql[0]:
db.update_mysql(mysql[0])
# 调用更新mysql的方法
sleep(2)
if "DELETE" in mysql[0]:
db.delete_mysql(mysql[0])
# 调用删除mysql的方法
sleep(2)
if mysql[1]:
mysql[1] = function_rn(mysql[1])
# 调用替换RN随机数字的方法
mysql[1] = function_rl(mysql[1])
# 调用替换RL随机字母的方法
if "SELECT" in mysql[1]:
mysql_result_tuple = db.query_mysql(mysql[1])
# mysql查询结果元祖
mysql_result_list = list(chain.from_iterable(mysql_result_tuple))
# 把二维元祖转换为一维列表
logger.info("发起请求之前mysql查询的结果列表为:{}", mysql_result_list)
if api:
api = function_sql(api, mysql_result_list)
# 调用替换MySQL查询结果的方法
if payload:
payload = function_sql(payload, mysql_result_list)
if headers:
headers = function_sql(headers, mysql_result_list)
if query_string:
query_string = function_sql(query_string, mysql_result_list)
if expected_result:
expected_result = function_sql(expected_result, mysql_result_list) if api:
api = function_rn(api)
api = function_rl(api)
if payload:
payload = function_rn(payload)
payload = function_rl(payload)
payload = demjson.decode(payload)
if headers:
headers = function_rn(headers)
headers = function_rl(headers)
headers = demjson.decode(headers)
if query_string:
query_string = function_rn(query_string)
query_string = function_rl(query_string)
query_string = demjson.decode(query_string) url = service_domain + api
# 拼接完整地址 logger.info("请求方式为:{}", request_mode)
logger.info("地址为:{}", url)
logger.info("请求体为:{}", payload)
logger.info("请求头为:{}", headers)
logger.info("请求参数为:{}", query_string)
logger.info("预期的响应代码为:{}", expected_code)
logger.info("预期的响应结果为:{}", expected_result) response = requests.request(
request_mode, url, data=json.dumps(payload),
headers=headers, params=query_string, timeout=(12, 18)
)
# 发起HTTP请求
# json.dumps()序列化把字典转换成字符串,json.loads()反序列化把字符串转换成字典
# data请求体为字符串,headers请求头与params请求参数为字典 actual_time = response.elapsed.total_seconds()
# 实际的响应时间
actual_code = response.status_code
# 实际的响应代码
actual_result_text = response.text
# 实际的响应结果(文本格式) if mysql:
if mysql[2]:
mysql[2] = function_rn(mysql[2])
mysql[2] = function_rl(mysql[2])
if "SELECT" in mysql[2]:
db_after = ConnectMySQL()
mysql_result_tuple_after = db_after.query_mysql(mysql[2])
mysql_result_list_after = list(chain.from_iterable(mysql_result_tuple_after))
logger.info("发起请求之后mysql查询的结果列表为:{}", mysql_result_list_after) logger.info("实际的响应代码为:{}", actual_code)
logger.info("实际的响应结果为:{}", actual_result_text)
logger.info("实际的响应时间为:{}", actual_time) if regular:
# 如果正则不为空
extract_list = []
# 定义一个提取结果列表
for i in regular["expression"]:
regular_result = re.findall(i, actual_result_text)[0]
# re.findall(正则表达式, 实际的响应结果)返回一个符合规则的list,取第1个
extract_list.append(regular_result)
# 把提取结果添加到提取结果列表里面
temporary_dict = dict(zip(regular["variable"], extract_list))
# 把变量列表与提取结果列表转为一个临时字典
for key, value in temporary_dict.items():
self.variable_result_dict[key] = value
# 把临时字典合并到变量名与提取的结果字典,已去重
else:
pass for key in list(self.variable_result_dict.keys()):
if not self.variable_result_dict[key]:
del self.variable_result_dict[key]
# 删除变量名与提取的结果字典中为空的键值对 expected_result = re.sub("{|}|\'|\"|\\[|\\]| ", "", expected_result)
actual_result_text = re.sub("{|}|\'|\"|\\[|\\]| ", "", actual_result_text)
# 去除大括号{、}、单引号'、双引号"、中括号[、]与空格
expected_result_list = re.split(":|,", expected_result)
actual_result_list = re.split(":|,", actual_result_text)
# 把文本转为列表,并去除:与,
logger.info("切割之后预期的响应结果列表为:{}", expected_result_list)
logger.info("切割之后实际的响应结果列表为:{}", actual_result_list) if expected_code == actual_code:
# 如果预期的响应代码等于实际的响应代码
if set(expected_result_list) <= set(actual_result_list):
# 判断是否是其真子集
logger.info("{}>>>预期的响应结果与实际的响应结果断言成功", case_name)
else:
logger.error("{}>>>预期的响应结果与实际的响应结果断言失败!!!", case_name)
assume(set(expected_result_list) <= set(actual_result_list))
# 预期的响应结果与实际的响应结果是被包含关系
if mysql:
if mysql[2]:
if set(mysql_result_list_after) <= set(actual_result_list):
# 判断是否是其真子集
logger.info("{}>>>发起请求之后mysql查询结果与实际的响应结果断言成功", case_name)
else:
logger.error("{}>>>发起请求之后mysql查询结果与实际的响应结果断言失败!!!", case_name)
assume(set(mysql_result_list_after) <= set(actual_result_list))
# 发起请求之后mysql查询结果与实际的响应结果是被包含关系
logger.info("##########用例分隔符##########\n")
# 双重断言
else:
logger.error("{}>>>执行失败!!!", case_name)
logger.error("预期的响应代码与实际的响应代码不相等:{}!={}", expected_code, actual_code)
assume(expected_code == actual_code)
logger.info("##########用例分隔符##########\n") if __name__ == "__main__":
pytest.main()
project_config.py文件:
"""
整个工程的配置文件
""" import os
import sys
import time from loguru import logger parameter = sys.argv[1]
# 从命令行获取参数
if "--cmd=" in parameter:
parameter = parameter.replace("--cmd=", "")
else:
pass environment = os.getenv("measured_environment", parameter)
# 环境变量 if environment == "dev":
service_domain = "http://www.dev.com"
# 开发环境
db_host = 'mysql.dev.com'
db_port = 3306
elif environment == "test":
service_domain = "http://www.test.com"
# 测试环境
db_host = 'mysql.test.com'
db_port = 3307
elif environment == "pre":
service_domain = "http://www.pre.com"
# 预生产环境
db_host = 'mysql.pre.com'
db_port = 3308
elif environment == "formal":
service_domain = "https://www.formal.com"
# 生产环境
db_host = None
db_port = None db_user = 'root'
db_password = '123456'
db_database = ''
# MySQL数据库配置 current_path = os.path.dirname(os.path.dirname(__file__))
# 获取当前目录的父目录的绝对路径
# 也就是整个工程的根目录
case_path = os.path.join(current_path, "case")
# 测试用例的目录
yaml_path = os.path.join(current_path, "resource")
# yaml文件的目录
today = time.strftime("%Y-%m-%d", time.localtime())
# 年月日 report_path = os.path.join(current_path, "report")
# 测试报告的目录
if os.path.exists(report_path):
pass
else:
os.mkdir(report_path, mode=0o777) log_path = os.path.join(current_path, "log")
# 日志的目录
if os.path.exists(log_path):
pass
else:
os.mkdir(log_path, mode=0o777) logging_file = os.path.join(log_path, "log{}.log".format(today)) logger.add(
logging_file,
format="{time:YYYY-MM-DD HH:mm:ss}|{level}|{message}",
level="INFO",
rotation="500 MB",
encoding="utf-8",
)
# loguru日志配置 test_scenario = "测试场景:XXX接口测试"
test_story = "测试故事:XXX接口测试"
test_case_priority = ["blocker", "critical", "normal", "minor", "trivial"]
test_case_address = "http://www.testcase.com"
test_case_address_title = "XXX接口测试用例地址"
# allure配置 project_name = "XXX接口自动化测试"
swagger_address = "http://www.swagger.com/swagger-ui.html"
test_department = "测试部门:"
tester = "测试人员:"
# conftest配置 first_yaml = "demo_one.yaml"
# 第一个yaml文件
yamlpy接口测试框架的更多相关文章
- JAVA+Maven+TestNG搭建接口测试框架及实例
1.配置JDK 见另一篇博客:http://www.cnblogs.com/testlurunxiu/p/5933912.html 2.安装Eclipse以及TestNG Eclipse下载地址:ht ...
- ITF Demo代码(用VBScript构建的接口测试框架)
ITF Demo代码(用VBScript构建的接口测试框架) http://blog.csdn.net/testing_is_believing/article/details/20872629
- 基于LoadRunner构建接口测试框架
基于LoadRunner构建接口测试框架 http://www.docin.com/p-775544153.html
- 初探接口测试框架--python系列7
点击标题下「蓝色微信名」可快速关注 坚持的是分享,搬运的是知识,图的是大家的进步,没有收费的培训,没有虚度的吹水,喜欢就关注.转发(免费帮助更多伙伴)等来交流,想了解的知识请留言,给你带来更多价值,是 ...
- 初探接口测试框架--python系列2
点击标题下「蓝色微信名」可快速关注 坚持的是分享,搬运的是知识,图的是大家的进步,没有收费的培训,没有虚度的吹水,喜欢就关注.转发(免费帮助更多伙伴)等来交流,想了解的知识请留言,给你带来更多价值,是 ...
- 初探接口测试框架--python系列3
点击标题下「微信」可快速关注 坚持的是分享,搬运的是知识,图的是大家的进步,没有收费的培训,没有虚度的吹水,喜欢就关注.转发(免费帮助更多伙伴)等来交流,想了解的知识请留言,给你带来更多价值,是我们期 ...
- 初探接口测试框架--python系列4
点击标题下「蓝色微信名」可快速关注 坚持的是分享,搬运的是知识,图的是大家的进步,没有收费的培训,没有虚度的吹水,喜欢就关注.转发(免费帮助更多伙伴)等来交流,想了解的知识请留言,给你带来更多价值,是 ...
- 初探接口测试框架--python系列5
点击标题下「蓝色微信名」可快速关注 坚持的是分享,搬运的是知识,图的是大家的进步,没有收费的培训,没有虚度的吹水,喜欢就关注.转发(免费帮助更多伙伴)等来交流,想了解的知识请留言,给你带来更多价值,是 ...
- 初探接口测试框架--python系列6
点击标题下「蓝色微信名」可快速关注 坚持的是分享,搬运的是知识,图的是大家的进步,没有收费的培训,没有虚度的吹水,喜欢就关注.转发(免费帮助更多伙伴)等来交流,想了解的知识请留言,给你带来更多价值,是 ...
随机推荐
- 2015年3月26日 - Javascript MVC 框架DerbyJS DerbyJS 是一个 MVC 框架,帮助编写实时,交互的应用。
2015年3月26日 - Javascript MVC 框架DerbyJS DerbyJS 是一个 MVC 框架,帮助编写实时,交互的应用.
- 最新Pyecharts-基本图表
Pyecharts是由Echarts而来,Echarts是百度开源的数据可视化的库,适合用来做图表设计开发,当使用Python与Echarts结合时就产生了Pyecharts.可使用pip安装,默认是 ...
- JS-04-流程控制和循环
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...
- mysql --->mysql 外键总结
mysql 外键总结 1.设置外键MySQL ERROR 1005 错误 MySQL ERROR 1005 (主要是约束不一样导致的)例如: 1.两表外键的引用类型不一样,如主键是int外键是char ...
- 记一次阿里云LVM扩容与 LVM 相关知识学习
一.lvm 扩容 问题: 我们阿里云服务器有一个磁盘容量为 1T ,但是最近由于业务的扩增,磁盘容量已经不够了,需要增大磁盘的容量.磁盘挂载在 /home,使用的是 LVM.我们现在需要对磁盘进行扩容 ...
- Docker基础内容之资源限制
内存限制 --memory:内存限定,格式是数字加单位,单位可以为 B.K.M.G.最小为 4M. --memory-swap:交换分区大小限定 CPU限制 --cpus:表示分配给容器可用的cpu资 ...
- 安卓开发实战-记账本APP(三)
本次实现的是有关登录,注册和整体页面的改观,实现下方选项导致页面的切换效果. 利用到的技术有Sqlite数据库的增删改查,与fragment实现.由于暂时没有找到合适的图标,先借用微信的图标暂代一下. ...
- Ceph 存储集群7-故障排除
Ceph 仍在积极开发中,所以你可能碰到一些问题,需要评估 Ceph 配置文件.并修改日志和调试选项来纠正它. 一.日志记录和调试 般来说,你应该在运行时增加调试选项来调试问题:也可以把调试选项添加到 ...
- codeforces 540D Bad Luck Island (概率DP)
题意:会出石头.剪刀.布的人分别有r,s,p个,他们相互碰到的概率相同,输的人死掉,问最终活下去的人是三种类型的概率 设状态dp(i,j,k)为还有i个石头,j个剪刀,k个布时的概率,dp(r,s,p ...
- vs 搭配 Linux 开发
这是一篇翻译,为什么突然想翻译文章了呢,因为很多大佬们都说英语对程序员还是挺重要的,毕竟互联网的最新技术基本都在歪果仁那边,如果英语不好,不会看国外的文档的话,将会错失接触第一手资料的机会,失去很多先 ...