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/

yamlpy接口测试框架_第1张图片

 

yamlpy接口测试框架_第2张图片

 

3、文件举例:

README.md文件:

 1 # yamlpy  
 2 接口测试框架  
 3 
 4 
 5 # 一、思路         
 6 1、采用requests+PyMySQL+demjson+loguru+PyYAML+ruamel.yaml+pytest+pytest-html+allure-pytest+pytest-assume+pytest-rerunfailures+pytest-sugar+pytest-timeout  
 7 2、requests是发起HTTP请求的第三方库    
 8 3、PyMySQL是连接MySQL的第三方库   
 9 4、demjson是解析json的第三方库  
10 5、loguru是记录日志的第三方库  
11 6、PyYAML与ruamel.yaml是读写yaml文件的第三方库  
12 7、pytest是单元测试的第三方库  
13 8、pytest-html是生成html测试报告的插件  
14 9、allure-pytest是生成allure测试报告的插件  
15 10、pytest-assume是多重断言的插件  
16 11、pytest-rerunfailures是失败重跑的插件   
17 12、pytest-sugar是显示进度的插件  
18 13、pytest-timeout是设置超时时间的插件  
19 
20 
21 # 二、目录结构    
22 1、case是测试用例包              
23 2、log是日志目录         
24 3、report是测试报告的目录       
25 4、resource是yaml文件的目录      
26 5、setting是工程的配置文件包            
27 6、tool是常用方法的封装包         
28 
29 
30 # 三、yaml文件说明  
31 1、字段(命名和格式不可修改,顺序可以修改)  
32 case_name: 用例名称  
33 mysql: MySQL语句,-列表格式,顺序不可修改  
34 第一行:mysql[0]  
35 第二行:mysql[1]  
36 第三行:mysql[2]  
37 第一行为增删改语句,第二行为查语句,第三行为查语句(数据库双重断言)  
38 第一行是发起请求之前的动作,没有返回结果  
39 第二行是发起请求之前的动作,有返回结果,是为了动态传参  
40 第三行是发起请求之后的动作,有返回结果,但是不可用于动态传参,是为了断言实际的响应结果  
41 当不需要增删改查和双重断言时,三行都为空  
42 当只需要增删改时,第一行为增删改语句,第二行为空,第三行为空  
43 当只需要查时,第一行为空,第二行为查语句,第三行为空  
44 当只需要双重断言时,第一行为空,第二行为空,第三行为查语句  
45 request_mode: 请求方式  
46 api: 接口路径  
47 data: 请求体,缩进字典格式或者json格式     
48 headers: 请求头,缩进字典格式或者json格式    
49 query_string: 请求参数,缩进字典格式或者json格式    
50 expected_code: 预期的响应代码    
51 expected_result: 预期的响应结果,-列表格式、缩进字典格式或者json格式  
52 regular: 正则,缩进字典格式  
53 >>variable:变量名,-列表格式  
54 >>expression:表达式,-列表格式  
55 
56 2、参数化  
57 正则表达式提取的结果用${变量名}匹配,一条用例里面可以有多个  
58 MySQL查询语句返回的结果,即第二行mysql[1]返回的结果,用{__SQL索引}匹配  
59 即{__SQL0}、{__SQL1}、{__SQL2}、{__SQL3}。。。。。。一条用例里面可以有多个  
60 随机数字用{__RN位数},一条用例里面可以有多个   
61 随机英文字母用{__RL位数},一条用例里面可以有多个  
62 以上4种类型在一条用例里面可以混合使用  
63 ${变量名}的作用域是全局的,其它3种的作用域仅限该条用例  
64 
65 
66 # 四、运行  
67 在工程的根目录下执行命令  
68 pytest+--cmd=环境缩写  
69 pytest --cmd=dev  
70 pytest --cmd=test  
71 pytest --cmd=pre  
72 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接口测试框架)