API 接口自动化测试详细图文教程学习系列32--Allure测试报告2

测试学习记录,仅供参考!

测试报告allure

建议可借鉴:完善优化 Allure 测试报告显示内容自定义定制化展示 Allure 测试报告内容添加allure测试报告显示信息

三、allure报告定制化显示每一步测试执行结果

此时可以看到allure测试报告左边的结构信息已经优化的比较完善,已经有层次结构;接下来开始优化右边的报告显示信息,这里不需要在测试函数中实现,因为最终要执行测试用例的共同方法,所以写在apiutils.py 文件公共方法中来实现;

复制代码
# 显示 一句话的 使用 TEXT 文本格式
allure.attach(url, '接口地址', attachment_type=allure.attachment_type.TEXT)

# 字典 使用 JSON 格式--因为第一个参数body是字符串类型,所以得 先转换成字符串格式
allure.attach(json.dumps(headers), f'请求头:{headers}', attachment_type=allure.attachment_type.JSON)

16、修改项目"unit_tools"软件包下的"apiutils.py"文件内容,先导包引入 allure 模块,因为是在公共方法中添加的,所以每个测试接口执行时都会添加;

复制代码
# 导包
import json
import jsonpath
from unit_tools.handle_data.yaml_handler import read_yaml, write_yaml
import re
from unit_tools.debugtalk import DebugTalk
# 引入 配置文件模块
from unit_tools.handle_data.configParse import ConfigParser
# 引入 发起请求模块
from unit_tools.sendrequests import SendRequests
# 引入 断言类
from unit_tools.assertion_utils import Assertions
# 导入模块
import traceback
import allure

# 创建一个类 RequestsBase
class RequestsBase:

    # 在RequestsBase类的里面写一个初始化构造函数
    def __init__(self):
        # 实例化一下,实例化之后就可以通过这个对象去获取 host
        self.conf = ConfigParser()
        # 实例化
        self.send_request = SendRequests()
        # 实例化断言类
        self.asserts = Assertions()

    # 创建一个方法 parse_and_replace_variables(解析并且替换变量)--传一个参数 yml_data--要解析的yaml文件的数组
    def parse_and_replace_variables(self, yml_data):
        """
        解析并替换YAML文件数据中的变量引用,如:${get_extract_data(goodsId,1)}
        :param yml_data: 解析的YAML数据
        :return: 最终要返回的是dict类型--才能给后续的一些方法调用时使用
        """
        # if isinstance(yml_data, str)--判断传进来的数据是什么类型?--后续需要字符串才能做操作的,所以这里先判断是不是字符串格式
        # yml_data若是字符串则执行 if 语句左边的代码 直接返回yml_data
        # yml_data不是字符串--调用json.dumps(yml_data)模块转成字符串--ensure_ascii=False 参数是可以处理中文
        # if else 语句写在一行是 三元运算--通过这一系列之后最终返回一个 新的变量yaml_data_str --它是一个字符串
        yml_data_str = yml_data if isinstance(yml_data, str) else json.dumps(yml_data, ensure_ascii=False)
        # 打印查看 yaml_data_str
        # print(f'解析前:{yml_data_str}')
        # 用for循环去判断 字符串yml_data_str里面这种标识 "${" 格式出现的次数--加一个'_'下划线表示不需要用到这个变量,是一个占位符的意思
        for _ in range(yml_data_str.count("${")):
            # if判断这个标识'${' 它包含在字符串yml_data_str里面--并且另一半它 '}' 也包含在字符串yml_data_str里面
            if '${' in yml_data_str and '}' in yml_data_str:
                # 通过获取到 "${" 标识的起始位置--通过字符串 yml_data_str 的索引 index--获取它的索引的起始位置
                strat_index = yml_data_str.index("${")
                # 获取结束位置 标识 "}"--再加一个参数 它的开头 strat_index
                end_index = yml_data_str.index("}", strat_index)
                # 拿到开始、结束位置后--通过字符串切片--起始位置、结束位置+1--为什么+1?--因为python中索引切片是左闭右开的一个规则
                # 什么是左闭右开?--开始位置是包含在切片里面的,但是结束元素不包含在切片中,所以得+1
                variable_data = yml_data_str[strat_index:end_index+1]

                # 使用正则表达式提取函数名和参数
                # 导入re模块--调用re模块中的match方法--里面跟两个参数,第一个参数是正则表达式,第二个参数是要提取正则表达式的字符串
                match = re.match(r'\$\{(\w+)\((.*?)\)\}', variable_data)
                # 判断一下有没有匹配到--匹配的对象是不是成功了--成功
                if match:
                    # 通过match.groups()去拿到函数名还有参数--定义两个变量去接收,第一个函数名func_name、第二个函数值func_params
                    func_name, func_params = match.groups()

                    # 先判断它存在--走if左边的代码--给它做一个切片;若参数值不存在,则返回一个空列表即可
                    # 参数值有一个或多个--所以需要做一个处理--参数值func_params通过逗号去做一个切片
                    # 最后再把值重新赋值给它自己
                    func_params = func_params.split(',') if func_params else []

                    # 使用面向对象反射getattr调用函数
                    # 引如类后直接 实例化类--第二个参数传的是一个函数名 func_name--函数调用 (*func_params)--通过星号把参数值func_params去解包传递给参数func_name
                    extract_data = getattr(DebugTalk(), func_name)(*func_params)
                    # print(f'提取到的结果:{extract_data}')
                    # 使用正则表达式替换原始字符串中的变量引用为调用后的结果
                    # 使用正则表达式替换函数re.sub--第一个参数就是需要替换的哪个参数值,第二个参数是解析后得到的数据extract_data,第三个参数是原始解析前的yml_data_str
                    yml_data_str = re.sub(re.escape(variable_data), str(extract_data), yml_data_str)
        # 还原数据,将其转换为字典类型
        # 添加异常处理--像这种转换的最好是加一个异常处理
        try:
            # 直接调用 son.loads(yml_data_str)--把字符串转换成字典
            data = json.loads(yml_data_str)
        # json转码异常
        except json.JSONDecodeError:
            # 返回原始数据yml_data_str--赋值给新变量 data
            data = yml_data_str
        return data

    # 写个方法 execute_test_cases--传一个参数 接口的信息--即写yaml文件信息
    def execute_test_cases(self, api_info):
        """
        规范yaml接口信息,执行接口、提取结果以及断言操作
        :param api_info:
        :return:
        """
        # 先打印查看api_info信息
        print(api_info)
        # 添加异常
        try:
            # 处理baseInfo里面的数据
            # 获取到接口的服务器地址--通过读取配置文件去获取--通过这个对象去调用
            conf_host = self.conf.get_host('host')
            # 获取url--完整的url是通过 服务器地址    加上  接口信息api_info
            url = conf_host + api_info['baseInfo']['url']
            allure.attach(url, '接口地址:', attachment_type=allure.attachment_type.TEXT)
            # 接口名称
            api_name = api_info['baseInfo']['api_name']
            allure.attach(api_name, f'接口名称:{api_name}', attachment_type=allure.attachment_type.TEXT)
            # 请求方式
            method = api_info['baseInfo']['method']
            allure.attach(method, f'请求方式:{method}', attachment_type=allure.attachment_type.TEXT)
            # 请求头--通过get方法获取看里面有没有请求头--如果有的话就返回给变量headers,若没有,则传个默认空值None就行
            # 判断请求头是否可选,使用get的话,当headers有的话就返回,没有时就返回None,不至于报错
            headers = api_info['baseInfo'].get('headers', None)
            # 再进行一步处理--判断 headers 不为空
            if headers is not None:
                # if isinstance(headers, str)    判断headers是什么类型?--如果是一个字符串str类型的话 就需要给它做一个解析
                # self.parse_and_replace_variables(headers)  调用parse_and_replace_variables方法,把headers给传进来
                # else headers   若不是字符串,直接给他返回headers
                # headers =    最后再把结果传回给它自己就行了
                # eval()    调用一个函数 把解析后的数据 self.parse_and_replace_variables(headers) 转换为 字典类型
                headers = eval(self.parse_and_replace_variables(headers)) if isinstance(headers, str) else headers
                # print(headers)
            allure.attach(json.dumps(headers), '请求头:', attachment_type=allure.attachment_type.JSON)

            # cookies 与 请求头一样
            cookies = api_info['baseInfo'].get('cookies', None)
            if cookies is not None:
                cookies = eval(self.parse_and_replace_variables(cookies)) if isinstance(cookies, str) else cookies
                # print(cookies)
            allure.attach(json.dumps(cookies), 'Cookies', attachment_type=allure.attachment_type.JSON)

            # 处理testCase里面的数据
            # 通过for循环--循环testcase--通过读取api_info--拿到最外层的数据testCase
            for testcase in api_info['testCase']:
                print(f'处理前:{testcase}')
                # 拿到之后,进行更近一步的处理--通过testcase.pop()去处理--先获取它第一组数据 case_name
                case_name = testcase.pop('case_name')
                allure.attach(case_name, f'测试用例:{case_name}', attachment_type=allure.attachment_type.TEXT)
                # 通过变量引用处理断言结果
                # 通过self.parse_and_replace_variables去断言解析的函数--通过for循环中testcase.get获取断言的关键词validation
                # 然后拿到validation后面的这组数据--再把断言的结果值给返回出去并赋值给变量val_result
                val_result = self.parse_and_replace_variables(testcase.get('validation'))
                # print(val_result)
                # 拿到断言结果,继续往下写--把testcase['validation']这个值更新成处理后的数据val_result
                testcase['validation'] = val_result
                # 同样是调用pop--然后去获取这个数据variables--最后把结果返回出去给variables
                validation = testcase.pop('validation')
                # print(validation, type(validation))
                # print(testcase, type(testcase))

                # 处理接口返回值提取部分
                # 通过testcase.pop去删除,删除之前再把结果返回,即返回再删除--获取extract关键词结果
                # --可以在跟一个参数,设置默认值,当login.yaml文件中有extract这个值的时候就返回,没有返回一个None,它就不会报错了
                # extract = testcase.pop('extract', None)
                # print(extract)
                # 处理为 列表的情况 extract_list--要把每一种情况都考虑进去
                # extract_list = testcase.pop('extract_list', None)
                # print(extract_list)
                # 把extract、extract_list 写在一行--看个人习惯,分开写或者一起都行
                extract, extract_list = testcase.pop('extract', None), testcase.pop('extract_list', None)
                print(f'处理后:{testcase}')

                # 处理参数类型和请求参数
                # 使用for循环--参数类型param_type、参数值param_value--它是一个字典直接通过testcase.items() 字典函数
                for param_type, param_value in testcase.items():
                    # 拿到之后开始进一步操作
                    # print(f'类型:{param_type}, 参数值:{param_value}')
                    # 先判断类型 param_type 在不在这三种其中之一--也就是说参数类型只能写这三种里面的一个
                    if param_type in ['params', 'data', 'json']:
                        # 若是在的话则可以解析了--直接去调用解析的方法--把参数值param_value传递给它--最后再把结果返回出去赋值给变量request_params
                        request_params = self.parse_and_replace_variables(param_value)
                        # print(request_params)
                        # 把testcase字典key值 参数类型更新为 通过解析之后的数据 request_params
                        testcase[param_type] = request_params
                print('处理之后:', testcase)

                # 文件上传处理
                files = testcase.pop('files', None)
                if files:
                    for fk, fv in files.items():
                        files = {fk: open(fv, mode='rb')}

                # 通过刚刚创建的实例化对象self.send_request去调用执行接口请求的方法execute_api_request()--里面的参数需要一一对应
                # **kwargs 未知数量传参------------直接把 **testcase 传给它
                response = self.send_request.execute_api_request(api_name=api_name, url=url, method=method, header=headers, case_name=case_name, cookie=cookies, file=files, **testcase)
                # 获取状态码和接口返回结果的text文本格式
                status_code, response_text = response.status_code, response.text
                print(response_text)

                # 处理接口返回值提取
                # 判断这个值extract不等于None的话
                if extract is not None:
                    # 就调用封装的方法extract_data--传递--第一个参数extract------------第二个参数response
                    self.extract_data(extract, response_text)
                # 判断这个值extract_list不等于None的话
                if extract_list is not None:
                    # 调用封装的方法extract_data_list--传递--第一个参数extract_list------------第二个参数response
                    self.extract_data_list(extract_list, response_text)

                # 处理接口断言
                self.asserts.assert_result(validation, response.json(), status_code)

        except Exception as e:
            # 因暂时未写日志模块,所以这里先打印出来
            # {str(traceback.format_exc())}--当出现异常时,会打印出比较详细的信息
            print(f'出现未知异常:--{str(traceback.format_exc())}')
            # 这里出现异常时,需手动抛出异常--在生成报告时,这里若没有抛出这个异常的话,在测试出现失败时,还是会统计为成功的
            raise e

    # 创建一个方法 extract_data 提取接口的返回值--接收两个参数,testcase_extract就是yaml文件中的token: $.token------------response_text 接口返回信息,文本格式
    @classmethod
    def extract_data(cls, testcase_extract, response_text):
        """
        提取单个参数,提取接口的返回参数,支持正则表达式提取和json提取器
        :param testcase_extract:    (dict)yaml文件中的extract值,例如:{'token':'$.token'}
        :param response_text:   (str)接口的实际返回值
        :return:
        """
        # 先打印一下表达式 {'token':'$.token'}
        # print(testcase_extract)
        extract_data = None
        try:
            # 使用for循环------------因为这个值token: $.token是一个字典类型,要分别去拿到要提取变量的名称token,还有后面拿到表达式$.token
            for key, value in testcase_extract.items():
                # any() 函数是 当里面的元素都为真True的情况下,就返回True------------则if通过,才会执行下面的语句------------这里其实是一个生成器表达式
                # 使用for循环里面的列表------------正则表达式(贪婪匹配、非贪婪匹配)
                # '(.*?)':只是匹配0次或多次; '(.+?)':只是匹配1次或多次;
                # r'(\d+)'、r'(\d*)':加 r 是防止被转义;\d 是获取数字;+ 加号是获取至少1次或多次;* 星号是至少0次
                if any(pat in value for pat in ['(.*?)', '(.+?)', r'(\d+)', r'(\d*)']):
                    # 使用正则表达式的模块调用search函数去全局的搜索--第一个参数是它的正则表达式value,第二个是从接口的返回信息中去提取response_text
                    ext_list = re.search(value, response_text)
                    # 把数据存储下来,存到字典中--对值value进行一个判断--判断提取到的数据是字符串还是整数类型
                    # 先去判断 if r'(\d+)' in value 有没有这个值,有的话把它转成整数类型的 int(ext_list.group(1))
                    # 若不是 (\d+) 则直接执行 else 后面的语句 ext_list.group(1)
                    extract_data = {key: int(ext_list.group(1)) if r'(\d+)' in value else ext_list.group(1)}
                    # print(f'提取到的结果:{extract_data}')
                # 判断有没有 $ 符号
                elif '$' in value:
                    # 引入模块 import jsonpath --调用jsonpath()去提取--因为json提取需要json格式,而response_text是字符串类型的
                    # 使用json.loads(response_text)转换成字典类型,它才能提取--第二个值value--它最外层是列表,所以需要索引[0]取值
                    extract_json = jsonpath.jsonpath(json.loads(response_text), value)[0]
                    # 拿到接口返回值token
                    print(extract_json)
                    # 先判断提取的结果是否存在--若存在,则定义一个字典存储起来;若不存在,则直接提示打印一句话
                    extract_data = {key: extract_json} if extract_json else {key: "未提取到数据,请检查接口返回信息或表达式!"}
                    # 提取到的结果
                    print(extract_data)
                # 判断extract_data是否存在
                if extract_data:
                    # 引入写入方法 write_yaml()--若存在,则调用方法--把结果字典类型的extract_data给存进去
                    write_yaml(extract_data)

        except re.error:
            print('正则表达式解析错误,请检查yaml文件extract表达式是否正确!')
        except json.JSONDecodeError:
            print('JSON解析错误,请检查yaml文件extract表达式是否正确!')

    @classmethod
    def extract_data_list(cls, testcase_extract_list, response_text):
        """
        提取多个参数,提取接口的返回参数,支持正则表达式提取和json提取器
        :param testcase_extract_list:    (dict)yaml文件中的extract_list值,例如:{'token':'$.token'}
        :param response_text:   (str)接口的实际返回值
        :return:
        """
        extract_data = None
        try:
            # 跟上面的方法一样,先去循环拿到key值、value值
            for key, value in testcase_extract_list.items():
                if any(pat in value for pat in ['(.*?)', '(.+?)', r'(\d+)', r'(\d*)']):
                    # 使用正则表达式的模块调用findall函数--传参还是一样的-- re.S --re模块的一个选项,表示在模式中匹配任何字符,包括换行符
                    ext_list = re.findall(value, response_text, re.S)
                    print(ext_list)
                    # 判断提取的结果对不对
                    if ext_list:
                        # 存储起来
                        extract_data = {key: ext_list}
                        print(extract_data)
                # json提取器
                elif "$" in value:
                    # 这里是提取多个,是一个列表,所以不需要通过索引[0]去取值
                    extract_json = jsonpath.jsonpath(json.loads(response_text), value)
                    print(extract_json)
                    # 判断提取的这个值是否存在
                    if extract_json:
                        extract_data = {key: extract_json}
                    else:
                        extract_data= {key: '未提取到数据,请检查接口返回信息或表达式!'}
                # 判断extract_data是否存在
                if extract_data:
                    # 写入方法
                    write_yaml(extract_data)

        except re.error:
            print('正则表达式解析错误,请检查yaml文件extract表达式是否正确!')
        except json.JSONDecodeError:
            print('JSON解析错误,请检查yaml文件extract表达式是否正确!')

# 测试调试查看
if __name__ == '__main__':
    # 先引进读取方法 read_yaml--传如一个它的相对路径--赋值给新变量data--[0]通过索引去列表值第一个
    api_info = read_yaml('../testcases/login/login.yaml')[0]
    # 实例化类 RequestsBase()--并赋值给一个变量对象 req
    req = RequestsBase()
    # 通过类对象去调用parse_and_replace_variables(data) 这个解析的方法--把读取到的 data 这个yaml数据传给它
    # res = req.parse_and_replace_variables(data)
    # print(f'解析后:{res}')
    # 通过这个对象req去调用刚刚封装的方法execute_test_cases--把读取到yaml文件的数据api_info传给它
    req.execute_test_cases(api_info)

断言先不在这处理展示了,因为这个文件处理的是yaml文件数据,所以展示的只有yaml文件中的预期结果;需要在测试报告中显示更加详细的断言结果,所以可以在封装断言的类中实现;

17、修改项目"unit_tools"软件包下的"assertion_utils.py"文件内容;

复制代码
# -*- coding:utf-8 -*-
# 引入内置模块--类型检测工具--指定类型
from typing import Callable,Any
# 引入自定义异常
# from exception_utils.exception import AssertTypeError
from unit_tools.exception_utils.exception import AssertTypeError
from unit_tools.db_connector.connectMysql import ConnectMysql
import jsonpath
import operator
import allure

# 创建一个断言类 Assertions--里面有多种断言模式
class Assertions:
    """
    接口断言模式封装
    1、状态码断言
    2、包含模式断言
    3、相等断言
    4、不相等断言
    5、数据库断言
    """
    # 封装成一个类方法 @classmethod--说明这个方法不需要访问实例属性
    @classmethod
    # 定义一个方法status_code_assert--跟上两个参数
    def status_code_assert(cls, expected_result, status_code):
        """
        接口的响应状态码断言
        :param expected_result: yaml文件code模式中的预期状态码
        :param status_code: 接口实际返回的状态码
        :return:
        """
        # 断言状态码标识,0 表示成功,其他表示失败
        failure_count = 0
        # 先判断它如果不是一个整数类型--使用isinstance判断数据类型函数来判断expected_result不是整数类型
        if not isinstance(expected_result, int):
            # 直接调用int方法给expected_result预期结果转换成整数类型,然后把转化之后的数据再传给它自己
            expected_result = int(expected_result)

        # 直接判断 预期结果 等于 接口的实际返回状态码
        if expected_result == status_code:
            print(f'状态码断言成功:接口实际返回状态码 {status_code} == {expected_result}')
            allure.attach(f'预期结果:{str(expected_result)}\n实际结果:{str(status_code)}', '状态码断言结果:成功!',
                          attachment_type=allure.attachment_type.TEXT)
        else:
            print(f'状态码断言失败:接口实际返回状态码 {status_code} == {expected_result}')
            # 标识加1--加1之后就不等于0了,表示失败
            failure_count += 1
            allure.attach(f'预期结果:{str(expected_result)}\n实际结果:{str(status_code)}', '状态码断言结果:失败!',
                          attachment_type=allure.attachment_type.TEXT)
        # 最后把失败统计的标识返回出去
        return failure_count

    @classmethod
    # 包含模式断言
    def contain_assert(cls, expected_result, response):
        """
        字符串包含模式,断言预期结果字符串是否包含在接口的实际响应返回信息中
        :param expected_result: yaml文件中的预期结果
        :param response: 接口的实际返回值
        :return:
        """
        failure_count = 0
        # 调用字典中的items()分别拿到key值、value值
        for assert_key, assert_value in expected_result.items():
            # print(f'key值:{assert_key} -- value值:{assert_value}')
            # 通过调用jsonpath.jsonpath()方法去接口的实际响应结果response中 根据表达式 $.. 去提取
            response_list = jsonpath.jsonpath(response, f'$..{assert_key}')
            # print(response_list)
            # 判断提取到的结果存不存在--并且是不是字符串类型
            if response_list and isinstance(response_list[0], str):
                # 如果是字符串类型的话,则重新组成一个字符串--把list数据转成字符串
                response_str = ''.join(response_list)
                # print(response_str)
                success_message = f'包含模式断言成功:预期结果【{assert_value}】存在实际结果【{response_str}】中'
                failure_message = f'包含模式断言失败:预期结果【{assert_value}】未存在,没有在实际结果【{response_str}】中找到'
                # 判断assert_value是不是包含在response_str里面
                if assert_value in response_str:
                    print(success_message)
                    allure.attach(f'预期结果:{str(assert_value)}\n实际结果:{str(response_str)}',
                                  '包含断言结果:成功!', attachment_type=allure.attachment_type.JSON)
                else:
                    failure_count += 1
                    print(failure_message)
                    allure.attach(f'预期结果:{str(assert_value)}\n实际结果:{str(response_str)}',
                                  '包含断言结果:失败!', attachment_type=allure.attachment_type.JSON)
        return failure_count

    @classmethod
    def equal_assert(cls, expected_result, response):
        """
        相等断言,根据yaml里面的validation关键词下面的eq模式数据去跟接口实际响应信息对比
        :param expected_result: (dict)yaml里面的eq值
        :param response: (dict)接口实际响应结果
        :return:
        """
        failure_count = 0
        if isinstance(response, dict) and isinstance(expected_result, dict):
            # print(f'yaml的预期结果:{expected_result}')
            # print(f'接口的实际结果:{response}')
            # 找出实际结果与预期结果共同的key值
            common_key = list(expected_result.keys() & response.keys())
            if common_key:
                common_key = common_key[0]
                # print(common_key)
                # 根据相同的key值去实际结果中获取,并重新生成一个实际结果的字典
                new_actual_result = {common_key: response[common_key]}
                # print(new_actual_result)
                # 调用operator.eq()方法
                eq_assert = operator.eq(new_actual_result, expected_result)
                if eq_assert:
                    print(f'相等断言成功:接口实际结果 {new_actual_result} == 预期结果:{expected_result}')
                    allure.attach(f'预期结果:{str(expected_result)}\n实际结果:{str(new_actual_result)}',
                                  '相等断言结果:成功!', attachment_type=allure.attachment_type.JSON)
                else:
                    failure_count += 1
                    print(f'相等断言失败:接口实际结果 {new_actual_result} != 预期结果:{expected_result}')
                    allure.attach(f'预期结果:{str(expected_result)}\n实际结果:{str(new_actual_result)}',
                                  '相等断言结果:失败!', attachment_type=allure.attachment_type.JSON)
            else:
                failure_count += 1
                print('相等断言失败:请检查yaml文件eq模式的预期结果或接口返回值是否正确!')

        return failure_count

    @classmethod
    def not_equal_assert(cls, expected_result, response):
        """
        不相等断言,根据yaml里面的validation关键词下面的ne模式数据去跟接口实际响应信息对比
        :param expected_result: (dict)yaml里面的ne值
        :param response: (dict)接口实际响应结果
        :return:
        """
        failure_count = 0
        if isinstance(response, dict) and isinstance(expected_result, dict):
            # print(f'yaml的预期结果:{expected_result}')
            # print(f'接口的实际结果:{response}')
            # 找出实际结果与预期结果共同的key值
            common_key = list(expected_result.keys() & response.keys())
            if common_key:
                common_key = common_key[0]
                # print(common_key)
                # 根据相同的key值去实际结果中获取,并重新生成一个实际结果的字典
                new_actual_result = {common_key: response[common_key]}
                # print(new_actual_result)
                # 调用operator.ne()方法
                eq_assert = operator.ne(new_actual_result, expected_result)
                if eq_assert:
                    print(f'不相等断言成功:接口实际结果 {new_actual_result} != 预期结果:{expected_result}')
                    allure.attach(f'预期结果:{str(expected_result)}\n实际结果:{str(new_actual_result)}',
                                  '不相等断言结果:成功!', attachment_type=allure.attachment_type.JSON)
                else:
                    failure_count += 1
                    print(f'不相等断言失败:接口实际结果 {new_actual_result} == 预期结果:{expected_result}')
                    allure.attach(f'预期结果:{str(expected_result)}\n实际结果:{str(new_actual_result)}',
                                  '不相等断言结果:失败!', attachment_type=allure.attachment_type.JSON)
            else:
                failure_count += 1
                print('不相等断言失败:请检查yaml文件ne模式的预期结果或接口返回值是否正确!')

        return failure_count

    @classmethod
    def database_assert(cls, expected_result, status_code=None):
        """
        数据库断言
        :param expected_result:  yaml文件db模式中的SQL语句预期结果
        :param status_code:  不做任何操作,仅做占位
        :return:
        """
        failure_count = 0
        conn = ConnectMysql()
        db_value = conn.query(expected_result)
        if db_value is not None:
            print('数据库断言成功!')
        else:
            failure_count += 1
            print('数据库断言失败:请检查数据库是否存在该数据!')
        return failure_count

    @classmethod
    # 定义一个方法assert_result 用于管理多种断言模式
    def assert_result(cls, expected_result, response, status_code):
        """
        断言主函数,通过all_flag标记,如all_flag == 0 表示测试成功,否则为失败
        :param expected_result: (list)yaml文件validation关键词下面的预期结果
        :param response: (dict)接口的实际响应信息
        :param status_code: 接口的实际响应状态码
        :return:
        """
        all_flag = 0
        # 通过字典映射的关系去管理方法
        assert_methods = {
            'code': cls.status_code_assert,
            'contain': cls.contain_assert,
            'eq': cls.equal_assert,
            'ne': cls.not_equal_assert,
            'db': cls.database_assert
        }
        try:
            # 先打印查看一下预期结果是什么类型
            # print(f'打印结果:{expected_result}', '打印类型:', type(expected_result))
            print(f'打印结果:{expected_result}, 打印类型:', type(expected_result))
            # 通过for循环 拿到断言模式
            for yq in expected_result:
                # print(yq)
                # 先拿到 断言模式assert_mode,预期结果值assert_value--通过字典items()函数获取到每一个值
                for assert_mode, assert_value in yq.items():
                    # print(f'断言模式:{assert_mode}--预期值:{assert_value}')
                    # 通过assert_methods.get(assert_mode)它的字典去读取对应的断言方法
                    # 表示assert_method是一个接收两个参数,类型为Any表示可以是任意类型,并返回整数的可调用对象
                    assert_method:Callable[[Any, Any], int] = assert_methods.get(assert_mode)
                    # 判断获取到的值存在
                    if assert_method:
                        # 调用对应的断言方法,传递适当的参数
                        # if assert_mode == 'code':
                        if assert_mode in ['code', 'db']:
                            # 返回一下标识 flag
                            flag = assert_method(assert_value, status_code)
                        else:
                            flag = assert_method(assert_value, response)
                        all_flag += flag
                    else:
                        print(f'不支持{assert_mode}该断言模式')
                        # 手动抛出异常
                        raise AssertTypeError(f'不支持{assert_mode}该断言模式')
        except Exception as exceptions:
            # print(exceptions)
            # 抛出异常
            raise exceptions

        # 通过assert关键词去断言 标识all_flag 等于0即测试成功,不等于0测试失败
        assert all_flag == 0, '测试失败!'
        print('测试成功!')

18、重新运行项目根目录下主函数 run.py 文件,可以查看到 allure 测试报告右侧的测试步骤显示信息;

19、修改项目"unit_tools"软件包下的"apiutils.py"文件内容,添加输出接口实际响应结果信息;

复制代码
# 导包
import json
import jsonpath
from unit_tools.handle_data.yaml_handler import read_yaml, write_yaml
import re
from unit_tools.debugtalk import DebugTalk
# 引入 配置文件模块
from unit_tools.handle_data.configParse import ConfigParser
# 引入 发起请求模块
from unit_tools.sendrequests import SendRequests
# 引入 断言类
from unit_tools.assertion_utils import Assertions
# 导入模块
import traceback
import allure

# 创建一个类 RequestsBase
class RequestsBase:

    # 在RequestsBase类的里面写一个初始化构造函数
    def __init__(self):
        # 实例化一下,实例化之后就可以通过这个对象去获取 host
        self.conf = ConfigParser()
        # 实例化
        self.send_request = SendRequests()
        # 实例化断言类
        self.asserts = Assertions()

    # 创建一个方法 parse_and_replace_variables(解析并且替换变量)--传一个参数 yml_data--要解析的yaml文件的数组
    def parse_and_replace_variables(self, yml_data):
        """
        解析并替换YAML文件数据中的变量引用,如:${get_extract_data(goodsId,1)}
        :param yml_data: 解析的YAML数据
        :return: 最终要返回的是dict类型--才能给后续的一些方法调用时使用
        """
        # if isinstance(yml_data, str)--判断传进来的数据是什么类型?--后续需要字符串才能做操作的,所以这里先判断是不是字符串格式
        # yml_data若是字符串则执行 if 语句左边的代码 直接返回yml_data
        # yml_data不是字符串--调用json.dumps(yml_data)模块转成字符串--ensure_ascii=False 参数是可以处理中文
        # if else 语句写在一行是 三元运算--通过这一系列之后最终返回一个 新的变量yaml_data_str --它是一个字符串
        yml_data_str = yml_data if isinstance(yml_data, str) else json.dumps(yml_data, ensure_ascii=False)
        # 打印查看 yaml_data_str
        # print(f'解析前:{yml_data_str}')
        # 用for循环去判断 字符串yml_data_str里面这种标识 "${" 格式出现的次数--加一个'_'下划线表示不需要用到这个变量,是一个占位符的意思
        for _ in range(yml_data_str.count("${")):
            # if判断这个标识'${' 它包含在字符串yml_data_str里面--并且另一半它 '}' 也包含在字符串yml_data_str里面
            if '${' in yml_data_str and '}' in yml_data_str:
                # 通过获取到 "${" 标识的起始位置--通过字符串 yml_data_str 的索引 index--获取它的索引的起始位置
                strat_index = yml_data_str.index("${")
                # 获取结束位置 标识 "}"--再加一个参数 它的开头 strat_index
                end_index = yml_data_str.index("}", strat_index)
                # 拿到开始、结束位置后--通过字符串切片--起始位置、结束位置+1--为什么+1?--因为python中索引切片是左闭右开的一个规则
                # 什么是左闭右开?--开始位置是包含在切片里面的,但是结束元素不包含在切片中,所以得+1
                variable_data = yml_data_str[strat_index:end_index+1]

                # 使用正则表达式提取函数名和参数
                # 导入re模块--调用re模块中的match方法--里面跟两个参数,第一个参数是正则表达式,第二个参数是要提取正则表达式的字符串
                match = re.match(r'\$\{(\w+)\((.*?)\)\}', variable_data)
                # 判断一下有没有匹配到--匹配的对象是不是成功了--成功
                if match:
                    # 通过match.groups()去拿到函数名还有参数--定义两个变量去接收,第一个函数名func_name、第二个函数值func_params
                    func_name, func_params = match.groups()

                    # 先判断它存在--走if左边的代码--给它做一个切片;若参数值不存在,则返回一个空列表即可
                    # 参数值有一个或多个--所以需要做一个处理--参数值func_params通过逗号去做一个切片
                    # 最后再把值重新赋值给它自己
                    func_params = func_params.split(',') if func_params else []

                    # 使用面向对象反射getattr调用函数
                    # 引如类后直接 实例化类--第二个参数传的是一个函数名 func_name--函数调用 (*func_params)--通过星号把参数值func_params去解包传递给参数func_name
                    extract_data = getattr(DebugTalk(), func_name)(*func_params)
                    # print(f'提取到的结果:{extract_data}')
                    # 使用正则表达式替换原始字符串中的变量引用为调用后的结果
                    # 使用正则表达式替换函数re.sub--第一个参数就是需要替换的哪个参数值,第二个参数是解析后得到的数据extract_data,第三个参数是原始解析前的yml_data_str
                    yml_data_str = re.sub(re.escape(variable_data), str(extract_data), yml_data_str)
        # 还原数据,将其转换为字典类型
        # 添加异常处理--像这种转换的最好是加一个异常处理
        try:
            # 直接调用 son.loads(yml_data_str)--把字符串转换成字典
            data = json.loads(yml_data_str)
        # json转码异常
        except json.JSONDecodeError:
            # 返回原始数据yml_data_str--赋值给新变量 data
            data = yml_data_str
        return data

    # 写个方法 execute_test_cases--传一个参数 接口的信息--即写yaml文件信息
    def execute_test_cases(self, api_info):
        """
        规范yaml接口信息,执行接口、提取结果以及断言操作
        :param api_info:
        :return:
        """
        # 先打印查看api_info信息
        print(api_info)
        # 添加异常
        try:
            # 处理baseInfo里面的数据
            # 获取到接口的服务器地址--通过读取配置文件去获取--通过这个对象去调用
            conf_host = self.conf.get_host('host')
            # 获取url--完整的url是通过 服务器地址    加上  接口信息api_info
            url = conf_host + api_info['baseInfo']['url']
            allure.attach(url, '接口地址:', attachment_type=allure.attachment_type.TEXT)
            # 接口名称
            api_name = api_info['baseInfo']['api_name']
            allure.attach(api_name, f'接口名称:{api_name}', attachment_type=allure.attachment_type.TEXT)
            # 请求方式
            method = api_info['baseInfo']['method']
            allure.attach(method, f'请求方式:{method}', attachment_type=allure.attachment_type.TEXT)
            # 请求头--通过get方法获取看里面有没有请求头--如果有的话就返回给变量headers,若没有,则传个默认空值None就行
            # 判断请求头是否可选,使用get的话,当headers有的话就返回,没有时就返回None,不至于报错
            headers = api_info['baseInfo'].get('headers', None)
            # 再进行一步处理--判断 headers 不为空
            if headers is not None:
                # if isinstance(headers, str)    判断headers是什么类型?--如果是一个字符串str类型的话 就需要给它做一个解析
                # self.parse_and_replace_variables(headers)  调用parse_and_replace_variables方法,把headers给传进来
                # else headers   若不是字符串,直接给他返回headers
                # headers =    最后再把结果传回给它自己就行了
                # eval()    调用一个函数 把解析后的数据 self.parse_and_replace_variables(headers) 转换为 字典类型
                headers = eval(self.parse_and_replace_variables(headers)) if isinstance(headers, str) else headers
                # print(headers)
            allure.attach(json.dumps(headers), '请求头:', attachment_type=allure.attachment_type.JSON)

            # cookies 与 请求头一样
            cookies = api_info['baseInfo'].get('cookies', None)
            if cookies is not None:
                cookies = eval(self.parse_and_replace_variables(cookies)) if isinstance(cookies, str) else cookies
                # print(cookies)
            allure.attach(json.dumps(cookies), 'Cookies', attachment_type=allure.attachment_type.JSON)

            # 处理testCase里面的数据
            # 通过for循环--循环testcase--通过读取api_info--拿到最外层的数据testCase
            for testcase in api_info['testCase']:
                print(f'处理前:{testcase}')
                # 拿到之后,进行更近一步的处理--通过testcase.pop()去处理--先获取它第一组数据 case_name
                case_name = testcase.pop('case_name')
                allure.attach(case_name, f'测试用例:{case_name}', attachment_type=allure.attachment_type.TEXT)
                # 通过变量引用处理断言结果
                # 通过self.parse_and_replace_variables去断言解析的函数--通过for循环中testcase.get获取断言的关键词validation
                # 然后拿到validation后面的这组数据--再把断言的结果值给返回出去并赋值给变量val_result
                val_result = self.parse_and_replace_variables(testcase.get('validation'))
                # print(val_result)
                # 拿到断言结果,继续往下写--把testcase['validation']这个值更新成处理后的数据val_result
                testcase['validation'] = val_result
                # 同样是调用pop--然后去获取这个数据variables--最后把结果返回出去给variables
                validation = testcase.pop('validation')
                # print(validation, type(validation))
                # print(testcase, type(testcase))

                # 处理接口返回值提取部分
                # 通过testcase.pop去删除,删除之前再把结果返回,即返回再删除--获取extract关键词结果
                # --可以在跟一个参数,设置默认值,当login.yaml文件中有extract这个值的时候就返回,没有返回一个None,它就不会报错了
                # extract = testcase.pop('extract', None)
                # print(extract)
                # 处理为 列表的情况 extract_list--要把每一种情况都考虑进去
                # extract_list = testcase.pop('extract_list', None)
                # print(extract_list)
                # 把extract、extract_list 写在一行--看个人习惯,分开写或者一起都行
                extract, extract_list = testcase.pop('extract', None), testcase.pop('extract_list', None)
                print(f'处理后:{testcase}')

                # 处理参数类型和请求参数
                # 使用for循环--参数类型param_type、参数值param_value--它是一个字典直接通过testcase.items() 字典函数
                for param_type, param_value in testcase.items():
                    # 拿到之后开始进一步操作
                    # print(f'类型:{param_type}, 参数值:{param_value}')
                    # 先判断类型 param_type 在不在这三种其中之一--也就是说参数类型只能写这三种里面的一个
                    if param_type in ['params', 'data', 'json']:
                        # 若是在的话则可以解析了--直接去调用解析的方法--把参数值param_value传递给它--最后再把结果返回出去赋值给变量request_params
                        request_params = self.parse_and_replace_variables(param_value)
                        # print(request_params)
                        # 把testcase字典key值 参数类型更新为 通过解析之后的数据 request_params
                        testcase[param_type] = request_params
                print('处理之后:', testcase)

                # 文件上传处理
                files = testcase.pop('files', None)
                if files:
                    for fk, fv in files.items():
                        files = {fk: open(fv, mode='rb')}

                # 通过刚刚创建的实例化对象self.send_request去调用执行接口请求的方法execute_api_request()--里面的参数需要一一对应
                # **kwargs 未知数量传参------------直接把 **testcase 传给它
                response = self.send_request.execute_api_request(api_name=api_name, url=url, method=method, header=headers, case_name=case_name, cookie=cookies, file=files, **testcase)
                # 获取状态码和接口返回结果的text文本格式
                status_code, response_text = response.status_code, response.text
                print(f'接口实际返回结果:{response_text}')
                allure.attach(response.text, '接口实际响应信息', attachment_type=allure.attachment_type.JSON)

                # 处理接口返回值提取
                # 判断这个值extract不等于None的话
                if extract is not None:
                    # 就调用封装的方法extract_data--传递--第一个参数extract------------第二个参数response
                    self.extract_data(extract, response_text)
                # 判断这个值extract_list不等于None的话
                if extract_list is not None:
                    # 调用封装的方法extract_data_list--传递--第一个参数extract_list------------第二个参数response
                    self.extract_data_list(extract_list, response_text)

                # 处理接口断言
                self.asserts.assert_result(validation, response.json(), status_code)

        except Exception as e:
            # 因暂时未写日志模块,所以这里先打印出来
            # {str(traceback.format_exc())}--当出现异常时,会打印出比较详细的信息
            print(f'出现未知异常:--{str(traceback.format_exc())}')
            # 这里出现异常时,需手动抛出异常--在生成报告时,这里若没有抛出这个异常的话,在测试出现失败时,还是会统计为成功的
            raise e

    # 创建一个方法 extract_data 提取接口的返回值--接收两个参数,testcase_extract就是yaml文件中的token: $.token------------response_text 接口返回信息,文本格式
    @classmethod
    def extract_data(cls, testcase_extract, response_text):
        """
        提取单个参数,提取接口的返回参数,支持正则表达式提取和json提取器
        :param testcase_extract:    (dict)yaml文件中的extract值,例如:{'token':'$.token'}
        :param response_text:   (str)接口的实际返回值
        :return:
        """
        # 先打印一下表达式 {'token':'$.token'}
        # print(testcase_extract)
        extract_data = None
        try:
            # 使用for循环------------因为这个值token: $.token是一个字典类型,要分别去拿到要提取变量的名称token,还有后面拿到表达式$.token
            for key, value in testcase_extract.items():
                # any() 函数是 当里面的元素都为真True的情况下,就返回True------------则if通过,才会执行下面的语句------------这里其实是一个生成器表达式
                # 使用for循环里面的列表------------正则表达式(贪婪匹配、非贪婪匹配)
                # '(.*?)':只是匹配0次或多次; '(.+?)':只是匹配1次或多次;
                # r'(\d+)'、r'(\d*)':加 r 是防止被转义;\d 是获取数字;+ 加号是获取至少1次或多次;* 星号是至少0次
                if any(pat in value for pat in ['(.*?)', '(.+?)', r'(\d+)', r'(\d*)']):
                    # 使用正则表达式的模块调用search函数去全局的搜索--第一个参数是它的正则表达式value,第二个是从接口的返回信息中去提取response_text
                    ext_list = re.search(value, response_text)
                    # 把数据存储下来,存到字典中--对值value进行一个判断--判断提取到的数据是字符串还是整数类型
                    # 先去判断 if r'(\d+)' in value 有没有这个值,有的话把它转成整数类型的 int(ext_list.group(1))
                    # 若不是 (\d+) 则直接执行 else 后面的语句 ext_list.group(1)
                    extract_data = {key: int(ext_list.group(1)) if r'(\d+)' in value else ext_list.group(1)}
                    # print(f'提取到的结果:{extract_data}')
                # 判断有没有 $ 符号
                elif '$' in value:
                    # 引入模块 import jsonpath --调用jsonpath()去提取--因为json提取需要json格式,而response_text是字符串类型的
                    # 使用json.loads(response_text)转换成字典类型,它才能提取--第二个值value--它最外层是列表,所以需要索引[0]取值
                    extract_json = jsonpath.jsonpath(json.loads(response_text), value)[0]
                    # 拿到接口返回值token
                    print(extract_json)
                    # 先判断提取的结果是否存在--若存在,则定义一个字典存储起来;若不存在,则直接提示打印一句话
                    extract_data = {key: extract_json} if extract_json else {key: "未提取到数据,请检查接口返回信息或表达式!"}
                    # 提取到的结果
                    print(extract_data)
                # 判断extract_data是否存在
                if extract_data:
                    # 引入写入方法 write_yaml()--若存在,则调用方法--把结果字典类型的extract_data给存进去
                    write_yaml(extract_data)

        except re.error:
            print('正则表达式解析错误,请检查yaml文件extract表达式是否正确!')
        except json.JSONDecodeError:
            print('JSON解析错误,请检查yaml文件extract表达式是否正确!')

    @classmethod
    def extract_data_list(cls, testcase_extract_list, response_text):
        """
        提取多个参数,提取接口的返回参数,支持正则表达式提取和json提取器
        :param testcase_extract_list:    (dict)yaml文件中的extract_list值,例如:{'token':'$.token'}
        :param response_text:   (str)接口的实际返回值
        :return:
        """
        extract_data = None
        try:
            # 跟上面的方法一样,先去循环拿到key值、value值
            for key, value in testcase_extract_list.items():
                if any(pat in value for pat in ['(.*?)', '(.+?)', r'(\d+)', r'(\d*)']):
                    # 使用正则表达式的模块调用findall函数--传参还是一样的-- re.S --re模块的一个选项,表示在模式中匹配任何字符,包括换行符
                    ext_list = re.findall(value, response_text, re.S)
                    print(ext_list)
                    # 判断提取的结果对不对
                    if ext_list:
                        # 存储起来
                        extract_data = {key: ext_list}
                        print(extract_data)
                # json提取器
                elif "$" in value:
                    # 这里是提取多个,是一个列表,所以不需要通过索引[0]去取值
                    extract_json = jsonpath.jsonpath(json.loads(response_text), value)
                    print(extract_json)
                    # 判断提取的这个值是否存在
                    if extract_json:
                        extract_data = {key: extract_json}
                    else:
                        extract_data= {key: '未提取到数据,请检查接口返回信息或表达式!'}
                # 判断extract_data是否存在
                if extract_data:
                    # 写入方法
                    write_yaml(extract_data)

        except re.error:
            print('正则表达式解析错误,请检查yaml文件extract表达式是否正确!')
        except json.JSONDecodeError:
            print('JSON解析错误,请检查yaml文件extract表达式是否正确!')

# 测试调试查看
if __name__ == '__main__':
    # 先引进读取方法 read_yaml--传如一个它的相对路径--赋值给新变量data--[0]通过索引去列表值第一个
    api_info = read_yaml('../testcases/login/login.yaml')[0]
    # 实例化类 RequestsBase()--并赋值给一个变量对象 req
    req = RequestsBase()
    # 通过类对象去调用parse_and_replace_variables(data) 这个解析的方法--把读取到的 data 这个yaml数据传给它
    # res = req.parse_and_replace_variables(data)
    # print(f'解析后:{res}')
    # 通过这个对象req去调用刚刚封装的方法execute_test_cases--把读取到yaml文件的数据api_info传给它
    req.execute_test_cases(api_info)

20、重新运行项目根目录下主函数 run.py 文件,查看到 allure 测试报告右侧信息,测试步骤里面已经增加了"接口实际响应信息",但是当返回信息有很多数据的时候,返回的是一个字符串类型,不是JSON格式,没有缩进,不是很直观的展示;

21、修改项目"unit_tools"软件包下的"apiutils.py"文件内容,在最外层定义一个方法,专门去处理这种数据;

复制代码
# 导包
import json
import jsonpath
from unit_tools.handle_data.yaml_handler import read_yaml, write_yaml
import re
from unit_tools.debugtalk import DebugTalk
# 引入 配置文件模块
from unit_tools.handle_data.configParse import ConfigParser
# 引入 发起请求模块
from unit_tools.sendrequests import SendRequests
# 引入 断言类
from unit_tools.assertion_utils import Assertions
# 导入模块
import traceback
import allure

# 创建一个类 RequestsBase
class RequestsBase:

    # 在RequestsBase类的里面写一个初始化构造函数
    def __init__(self):
        # 实例化一下,实例化之后就可以通过这个对象去获取 host
        self.conf = ConfigParser()
        # 实例化
        self.send_request = SendRequests()
        # 实例化断言类
        self.asserts = Assertions()

    # 创建一个方法 parse_and_replace_variables(解析并且替换变量)--传一个参数 yml_data--要解析的yaml文件的数组
    def parse_and_replace_variables(self, yml_data):
        """
        解析并替换YAML文件数据中的变量引用,如:${get_extract_data(goodsId,1)}
        :param yml_data: 解析的YAML数据
        :return: 最终要返回的是dict类型--才能给后续的一些方法调用时使用
        """
        # if isinstance(yml_data, str)--判断传进来的数据是什么类型?--后续需要字符串才能做操作的,所以这里先判断是不是字符串格式
        # yml_data若是字符串则执行 if 语句左边的代码 直接返回yml_data
        # yml_data不是字符串--调用json.dumps(yml_data)模块转成字符串--ensure_ascii=False 参数是可以处理中文
        # if else 语句写在一行是 三元运算--通过这一系列之后最终返回一个 新的变量yaml_data_str --它是一个字符串
        yml_data_str = yml_data if isinstance(yml_data, str) else json.dumps(yml_data, ensure_ascii=False)
        # 打印查看 yaml_data_str
        # print(f'解析前:{yml_data_str}')
        # 用for循环去判断 字符串yml_data_str里面这种标识 "${" 格式出现的次数--加一个'_'下划线表示不需要用到这个变量,是一个占位符的意思
        for _ in range(yml_data_str.count("${")):
            # if判断这个标识'${' 它包含在字符串yml_data_str里面--并且另一半它 '}' 也包含在字符串yml_data_str里面
            if '${' in yml_data_str and '}' in yml_data_str:
                # 通过获取到 "${" 标识的起始位置--通过字符串 yml_data_str 的索引 index--获取它的索引的起始位置
                strat_index = yml_data_str.index("${")
                # 获取结束位置 标识 "}"--再加一个参数 它的开头 strat_index
                end_index = yml_data_str.index("}", strat_index)
                # 拿到开始、结束位置后--通过字符串切片--起始位置、结束位置+1--为什么+1?--因为python中索引切片是左闭右开的一个规则
                # 什么是左闭右开?--开始位置是包含在切片里面的,但是结束元素不包含在切片中,所以得+1
                variable_data = yml_data_str[strat_index:end_index+1]

                # 使用正则表达式提取函数名和参数
                # 导入re模块--调用re模块中的match方法--里面跟两个参数,第一个参数是正则表达式,第二个参数是要提取正则表达式的字符串
                match = re.match(r'\$\{(\w+)\((.*?)\)\}', variable_data)
                # 判断一下有没有匹配到--匹配的对象是不是成功了--成功
                if match:
                    # 通过match.groups()去拿到函数名还有参数--定义两个变量去接收,第一个函数名func_name、第二个函数值func_params
                    func_name, func_params = match.groups()

                    # 先判断它存在--走if左边的代码--给它做一个切片;若参数值不存在,则返回一个空列表即可
                    # 参数值有一个或多个--所以需要做一个处理--参数值func_params通过逗号去做一个切片
                    # 最后再把值重新赋值给它自己
                    func_params = func_params.split(',') if func_params else []

                    # 使用面向对象反射getattr调用函数
                    # 引如类后直接 实例化类--第二个参数传的是一个函数名 func_name--函数调用 (*func_params)--通过星号把参数值func_params去解包传递给参数func_name
                    extract_data = getattr(DebugTalk(), func_name)(*func_params)
                    # print(f'提取到的结果:{extract_data}')
                    # 使用正则表达式替换原始字符串中的变量引用为调用后的结果
                    # 使用正则表达式替换函数re.sub--第一个参数就是需要替换的哪个参数值,第二个参数是解析后得到的数据extract_data,第三个参数是原始解析前的yml_data_str
                    yml_data_str = re.sub(re.escape(variable_data), str(extract_data), yml_data_str)
        # 还原数据,将其转换为字典类型
        # 添加异常处理--像这种转换的最好是加一个异常处理
        try:
            # 直接调用 son.loads(yml_data_str)--把字符串转换成字典
            data = json.loads(yml_data_str)
        # json转码异常
        except json.JSONDecodeError:
            # 返回原始数据yml_data_str--赋值给新变量 data
            data = yml_data_str
        return data

    # 写个类方法,因为它不需要访问实例属性
    @classmethod
    def allure_attach_response(cls, response):
        """
        在allure测试报告中显示接口响应信息,让它展示JSON格式
        :param response:  传一个参数,响应结果信息
        :return:
        """
        # 判断响应信息是什么类型?
        if isinstance(response, dict):
            # 如果是字典类型,通过json反序列化json.dumps()转成字符串--显示中文ensure_ascii=False--显示缩进 indent=4
            allure_response = json.dumps(response, ensure_ascii=False, indent=4)
        else:
            # 如果不是字典,直接把响应结果response赋值给它allure_response
            allure_response = response
        # 最后把一个结果给返回出去
        return allure_response

    # 写个方法 execute_test_cases--传一个参数 接口的信息--即写yaml文件信息
    def execute_test_cases(self, api_info):
        """
        规范yaml接口信息,执行接口、提取结果以及断言操作
        :param api_info:
        :return:
        """
        # 先打印查看api_info信息
        print(api_info)
        # 添加异常
        try:
            # 处理baseInfo里面的数据
            # 获取到接口的服务器地址--通过读取配置文件去获取--通过这个对象去调用
            conf_host = self.conf.get_host('host')
            # 获取url--完整的url是通过 服务器地址    加上  接口信息api_info
            url = conf_host + api_info['baseInfo']['url']
            allure.attach(url, '接口地址:', attachment_type=allure.attachment_type.TEXT)
            # 接口名称
            api_name = api_info['baseInfo']['api_name']
            allure.attach(api_name, f'接口名称:{api_name}', attachment_type=allure.attachment_type.TEXT)
            # 请求方式
            method = api_info['baseInfo']['method']
            allure.attach(method, f'请求方式:{method}', attachment_type=allure.attachment_type.TEXT)
            # 请求头--通过get方法获取看里面有没有请求头--如果有的话就返回给变量headers,若没有,则传个默认空值None就行
            # 判断请求头是否可选,使用get的话,当headers有的话就返回,没有时就返回None,不至于报错
            headers = api_info['baseInfo'].get('headers', None)
            # 再进行一步处理--判断 headers 不为空
            if headers is not None:
                # if isinstance(headers, str)    判断headers是什么类型?--如果是一个字符串str类型的话 就需要给它做一个解析
                # self.parse_and_replace_variables(headers)  调用parse_and_replace_variables方法,把headers给传进来
                # else headers   若不是字符串,直接给他返回headers
                # headers =    最后再把结果传回给它自己就行了
                # eval()    调用一个函数 把解析后的数据 self.parse_and_replace_variables(headers) 转换为 字典类型
                headers = eval(self.parse_and_replace_variables(headers)) if isinstance(headers, str) else headers
                # print(headers)
            allure.attach(json.dumps(headers), '请求头:', attachment_type=allure.attachment_type.JSON)

            # cookies 与 请求头一样
            cookies = api_info['baseInfo'].get('cookies', None)
            if cookies is not None:
                cookies = eval(self.parse_and_replace_variables(cookies)) if isinstance(cookies, str) else cookies
                # print(cookies)
            allure.attach(json.dumps(cookies), 'Cookies', attachment_type=allure.attachment_type.JSON)

            # 处理testCase里面的数据
            # 通过for循环--循环testcase--通过读取api_info--拿到最外层的数据testCase
            for testcase in api_info['testCase']:
                print(f'处理前:{testcase}')
                # 拿到之后,进行更近一步的处理--通过testcase.pop()去处理--先获取它第一组数据 case_name
                case_name = testcase.pop('case_name')
                allure.attach(case_name, f'测试用例:{case_name}', attachment_type=allure.attachment_type.TEXT)
                # 通过变量引用处理断言结果
                # 通过self.parse_and_replace_variables去断言解析的函数--通过for循环中testcase.get获取断言的关键词validation
                # 然后拿到validation后面的这组数据--再把断言的结果值给返回出去并赋值给变量val_result
                val_result = self.parse_and_replace_variables(testcase.get('validation'))
                # print(val_result)
                # 拿到断言结果,继续往下写--把testcase['validation']这个值更新成处理后的数据val_result
                testcase['validation'] = val_result
                # 同样是调用pop--然后去获取这个数据variables--最后把结果返回出去给variables
                validation = testcase.pop('validation')
                # print(validation, type(validation))
                # print(testcase, type(testcase))

                # 处理接口返回值提取部分
                # 通过testcase.pop去删除,删除之前再把结果返回,即返回再删除--获取extract关键词结果
                # --可以在跟一个参数,设置默认值,当login.yaml文件中有extract这个值的时候就返回,没有返回一个None,它就不会报错了
                # extract = testcase.pop('extract', None)
                # print(extract)
                # 处理为 列表的情况 extract_list--要把每一种情况都考虑进去
                # extract_list = testcase.pop('extract_list', None)
                # print(extract_list)
                # 把extract、extract_list 写在一行--看个人习惯,分开写或者一起都行
                extract, extract_list = testcase.pop('extract', None), testcase.pop('extract_list', None)
                print(f'处理后:{testcase}')

                # 处理参数类型和请求参数
                # 使用for循环--参数类型param_type、参数值param_value--它是一个字典直接通过testcase.items() 字典函数
                for param_type, param_value in testcase.items():
                    # 拿到之后开始进一步操作
                    # print(f'类型:{param_type}, 参数值:{param_value}')
                    # 先判断类型 param_type 在不在这三种其中之一--也就是说参数类型只能写这三种里面的一个
                    if param_type in ['params', 'data', 'json']:
                        # 若是在的话则可以解析了--直接去调用解析的方法--把参数值param_value传递给它--最后再把结果返回出去赋值给变量request_params
                        request_params = self.parse_and_replace_variables(param_value)
                        # print(request_params)
                        # 把testcase字典key值 参数类型更新为 通过解析之后的数据 request_params
                        testcase[param_type] = request_params
                print('处理之后:', testcase)

                # 文件上传处理
                files = testcase.pop('files', None)
                if files:
                    for fk, fv in files.items():
                        files = {fk: open(fv, mode='rb')}

                # 通过刚刚创建的实例化对象self.send_request去调用执行接口请求的方法execute_api_request()--里面的参数需要一一对应
                # **kwargs 未知数量传参------------直接把 **testcase 传给它
                response = self.send_request.execute_api_request(api_name=api_name, url=url, method=method, header=headers, case_name=case_name, cookie=cookies, file=files, **testcase)
                # 获取状态码和接口返回结果的text文本格式
                status_code, response_text = response.status_code, response.text
                print(f'接口实际返回结果:{response_text}')
                # 直接调用self.allure_attach_response()--把响应结果.json()--response.json()给它传进去
                allure.attach(self.allure_attach_response(response.json()), '接口实际响应信息',
                              attachment_type=allure.attachment_type.JSON)

                # 处理接口返回值提取
                # 判断这个值extract不等于None的话
                if extract is not None:
                    # 就调用封装的方法extract_data--传递--第一个参数extract------------第二个参数response
                    self.extract_data(extract, response_text)
                # 判断这个值extract_list不等于None的话
                if extract_list is not None:
                    # 调用封装的方法extract_data_list--传递--第一个参数extract_list------------第二个参数response
                    self.extract_data_list(extract_list, response_text)

                # 处理接口断言
                self.asserts.assert_result(validation, response.json(), status_code)

        except Exception as e:
            # 因暂时未写日志模块,所以这里先打印出来
            # {str(traceback.format_exc())}--当出现异常时,会打印出比较详细的信息
            print(f'出现未知异常:--{str(traceback.format_exc())}')
            # 这里出现异常时,需手动抛出异常--在生成报告时,这里若没有抛出这个异常的话,在测试出现失败时,还是会统计为成功的
            raise e

    # 创建一个方法 extract_data 提取接口的返回值--接收两个参数,testcase_extract就是yaml文件中的token: $.token------------response_text 接口返回信息,文本格式
    @classmethod
    def extract_data(cls, testcase_extract, response_text):
        """
        提取单个参数,提取接口的返回参数,支持正则表达式提取和json提取器
        :param testcase_extract:    (dict)yaml文件中的extract值,例如:{'token':'$.token'}
        :param response_text:   (str)接口的实际返回值
        :return:
        """
        # 先打印一下表达式 {'token':'$.token'}
        # print(testcase_extract)
        extract_data = None
        try:
            # 使用for循环------------因为这个值token: $.token是一个字典类型,要分别去拿到要提取变量的名称token,还有后面拿到表达式$.token
            for key, value in testcase_extract.items():
                # any() 函数是 当里面的元素都为真True的情况下,就返回True------------则if通过,才会执行下面的语句------------这里其实是一个生成器表达式
                # 使用for循环里面的列表------------正则表达式(贪婪匹配、非贪婪匹配)
                # '(.*?)':只是匹配0次或多次; '(.+?)':只是匹配1次或多次;
                # r'(\d+)'、r'(\d*)':加 r 是防止被转义;\d 是获取数字;+ 加号是获取至少1次或多次;* 星号是至少0次
                if any(pat in value for pat in ['(.*?)', '(.+?)', r'(\d+)', r'(\d*)']):
                    # 使用正则表达式的模块调用search函数去全局的搜索--第一个参数是它的正则表达式value,第二个是从接口的返回信息中去提取response_text
                    ext_list = re.search(value, response_text)
                    # 把数据存储下来,存到字典中--对值value进行一个判断--判断提取到的数据是字符串还是整数类型
                    # 先去判断 if r'(\d+)' in value 有没有这个值,有的话把它转成整数类型的 int(ext_list.group(1))
                    # 若不是 (\d+) 则直接执行 else 后面的语句 ext_list.group(1)
                    extract_data = {key: int(ext_list.group(1)) if r'(\d+)' in value else ext_list.group(1)}
                    # print(f'提取到的结果:{extract_data}')
                # 判断有没有 $ 符号
                elif '$' in value:
                    # 引入模块 import jsonpath --调用jsonpath()去提取--因为json提取需要json格式,而response_text是字符串类型的
                    # 使用json.loads(response_text)转换成字典类型,它才能提取--第二个值value--它最外层是列表,所以需要索引[0]取值
                    extract_json = jsonpath.jsonpath(json.loads(response_text), value)[0]
                    # 拿到接口返回值token
                    print(extract_json)
                    # 先判断提取的结果是否存在--若存在,则定义一个字典存储起来;若不存在,则直接提示打印一句话
                    extract_data = {key: extract_json} if extract_json else {key: "未提取到数据,请检查接口返回信息或表达式!"}
                    # 提取到的结果
                    print(extract_data)
                # 判断extract_data是否存在
                if extract_data:
                    # 引入写入方法 write_yaml()--若存在,则调用方法--把结果字典类型的extract_data给存进去
                    write_yaml(extract_data)

        except re.error:
            print('正则表达式解析错误,请检查yaml文件extract表达式是否正确!')
        except json.JSONDecodeError:
            print('JSON解析错误,请检查yaml文件extract表达式是否正确!')

    @classmethod
    def extract_data_list(cls, testcase_extract_list, response_text):
        """
        提取多个参数,提取接口的返回参数,支持正则表达式提取和json提取器
        :param testcase_extract_list:    (dict)yaml文件中的extract_list值,例如:{'token':'$.token'}
        :param response_text:   (str)接口的实际返回值
        :return:
        """
        extract_data = None
        try:
            # 跟上面的方法一样,先去循环拿到key值、value值
            for key, value in testcase_extract_list.items():
                if any(pat in value for pat in ['(.*?)', '(.+?)', r'(\d+)', r'(\d*)']):
                    # 使用正则表达式的模块调用findall函数--传参还是一样的-- re.S --re模块的一个选项,表示在模式中匹配任何字符,包括换行符
                    ext_list = re.findall(value, response_text, re.S)
                    print(ext_list)
                    # 判断提取的结果对不对
                    if ext_list:
                        # 存储起来
                        extract_data = {key: ext_list}
                        print(extract_data)
                # json提取器
                elif "$" in value:
                    # 这里是提取多个,是一个列表,所以不需要通过索引[0]去取值
                    extract_json = jsonpath.jsonpath(json.loads(response_text), value)
                    print(extract_json)
                    # 判断提取的这个值是否存在
                    if extract_json:
                        extract_data = {key: extract_json}
                    else:
                        extract_data= {key: '未提取到数据,请检查接口返回信息或表达式!'}
                # 判断extract_data是否存在
                if extract_data:
                    # 写入方法
                    write_yaml(extract_data)

        except re.error:
            print('正则表达式解析错误,请检查yaml文件extract表达式是否正确!')
        except json.JSONDecodeError:
            print('JSON解析错误,请检查yaml文件extract表达式是否正确!')

# 测试调试查看
if __name__ == '__main__':
    # 先引进读取方法 read_yaml--传如一个它的相对路径--赋值给新变量data--[0]通过索引去列表值第一个
    api_info = read_yaml('../testcases/login/login.yaml')[0]
    # 实例化类 RequestsBase()--并赋值给一个变量对象 req
    req = RequestsBase()
    # 通过类对象去调用parse_and_replace_variables(data) 这个解析的方法--把读取到的 data 这个yaml数据传给它
    # res = req.parse_and_replace_variables(data)
    # print(f'解析后:{res}')
    # 通过这个对象req去调用刚刚封装的方法execute_test_cases--把读取到yaml文件的数据api_info传给它
    req.execute_test_cases(api_info)

22、重新运行项目根目录下主函数 run.py 文件,查看到 allure 测试报告右侧信息测试步骤里面"接口实际响应信息"已经按照设计样式展示数据信息;

四、allure 测试报告定制化添加显示参数类型

添加"入参"数据信息,把入参数据信息也加到 allure 测试报告里面去;

23、修改"apiutils.py"文件,还是在公共基类文件里面去实现,在执行全部execute_test_cases 测试用例时添加,先优化里面的方法"allure_attach_response"把它变成公共方法"allure_attach_dict_result",以减少代码量,后续所有需要把"字典"转换为"字符串"的都可以调用此方法;

复制代码
# 导包
import json
import jsonpath
from unit_tools.handle_data.yaml_handler import read_yaml, write_yaml
import re
from unit_tools.debugtalk import DebugTalk
# 引入 配置文件模块
from unit_tools.handle_data.configParse import ConfigParser
# 引入 发起请求模块
from unit_tools.sendrequests import SendRequests
# 引入 断言类
from unit_tools.assertion_utils import Assertions
# 导入模块
import traceback
import allure

# 创建一个类 RequestsBase
class RequestsBase:

    # 在RequestsBase类的里面写一个初始化构造函数
    def __init__(self):
        # 实例化一下,实例化之后就可以通过这个对象去获取 host
        self.conf = ConfigParser()
        # 实例化
        self.send_request = SendRequests()
        # 实例化断言类
        self.asserts = Assertions()

    # 创建一个方法 parse_and_replace_variables(解析并且替换变量)--传一个参数 yml_data--要解析的yaml文件的数组
    def parse_and_replace_variables(self, yml_data):
        """
        解析并替换YAML文件数据中的变量引用,如:${get_extract_data(goodsId,1)}
        :param yml_data: 解析的YAML数据
        :return: 最终要返回的是dict类型--才能给后续的一些方法调用时使用
        """
        # if isinstance(yml_data, str)--判断传进来的数据是什么类型?--后续需要字符串才能做操作的,所以这里先判断是不是字符串格式
        # yml_data若是字符串则执行 if 语句左边的代码 直接返回yml_data
        # yml_data不是字符串--调用json.dumps(yml_data)模块转成字符串--ensure_ascii=False 参数是可以处理中文
        # if else 语句写在一行是 三元运算--通过这一系列之后最终返回一个 新的变量yaml_data_str --它是一个字符串
        yml_data_str = yml_data if isinstance(yml_data, str) else json.dumps(yml_data, ensure_ascii=False)
        # 打印查看 yaml_data_str
        # print(f'解析前:{yml_data_str}')
        # 用for循环去判断 字符串yml_data_str里面这种标识 "${" 格式出现的次数--加一个'_'下划线表示不需要用到这个变量,是一个占位符的意思
        for _ in range(yml_data_str.count("${")):
            # if判断这个标识'${' 它包含在字符串yml_data_str里面--并且另一半它 '}' 也包含在字符串yml_data_str里面
            if '${' in yml_data_str and '}' in yml_data_str:
                # 通过获取到 "${" 标识的起始位置--通过字符串 yml_data_str 的索引 index--获取它的索引的起始位置
                strat_index = yml_data_str.index("${")
                # 获取结束位置 标识 "}"--再加一个参数 它的开头 strat_index
                end_index = yml_data_str.index("}", strat_index)
                # 拿到开始、结束位置后--通过字符串切片--起始位置、结束位置+1--为什么+1?--因为python中索引切片是左闭右开的一个规则
                # 什么是左闭右开?--开始位置是包含在切片里面的,但是结束元素不包含在切片中,所以得+1
                variable_data = yml_data_str[strat_index:end_index+1]

                # 使用正则表达式提取函数名和参数
                # 导入re模块--调用re模块中的match方法--里面跟两个参数,第一个参数是正则表达式,第二个参数是要提取正则表达式的字符串
                match = re.match(r'\$\{(\w+)\((.*?)\)\}', variable_data)
                # 判断一下有没有匹配到--匹配的对象是不是成功了--成功
                if match:
                    # 通过match.groups()去拿到函数名还有参数--定义两个变量去接收,第一个函数名func_name、第二个函数值func_params
                    func_name, func_params = match.groups()

                    # 先判断它存在--走if左边的代码--给它做一个切片;若参数值不存在,则返回一个空列表即可
                    # 参数值有一个或多个--所以需要做一个处理--参数值func_params通过逗号去做一个切片
                    # 最后再把值重新赋值给它自己
                    func_params = func_params.split(',') if func_params else []

                    # 使用面向对象反射getattr调用函数
                    # 引如类后直接 实例化类--第二个参数传的是一个函数名 func_name--函数调用 (*func_params)--通过星号把参数值func_params去解包传递给参数func_name
                    extract_data = getattr(DebugTalk(), func_name)(*func_params)
                    # print(f'提取到的结果:{extract_data}')
                    # 使用正则表达式替换原始字符串中的变量引用为调用后的结果
                    # 使用正则表达式替换函数re.sub--第一个参数就是需要替换的哪个参数值,第二个参数是解析后得到的数据extract_data,第三个参数是原始解析前的yml_data_str
                    yml_data_str = re.sub(re.escape(variable_data), str(extract_data), yml_data_str)
        # 还原数据,将其转换为字典类型
        # 添加异常处理--像这种转换的最好是加一个异常处理
        try:
            # 直接调用 son.loads(yml_data_str)--把字符串转换成字典
            data = json.loads(yml_data_str)
        # json转码异常
        except json.JSONDecodeError:
            # 返回原始数据yml_data_str--赋值给新变量 data
            data = yml_data_str
        return data

    # 写个类方法,因为它不需要访问实例属性
    @classmethod
    def allure_attach_dict_result(cls, result):
        """
        在allure测试报告中显示接口响应信息,让它展示JSON格式;处理结果是字典类型,就将其转化成字符串,并做格式化处理,否则直接返回
        :param response:  传一个参数,响应结果信息
        :return:
        """
        # 判断响应信息是什么类型?
        if isinstance(result, dict):
            # 如果是字典类型,通过json反序列化json.dumps()转成字符串--显示中文ensure_ascii=False--显示缩进 indent=4
            allure_response = json.dumps(result, ensure_ascii=False, indent=4)
        else:
            # 如果不是字典,直接把响应结果result赋值给它allure_response
            allure_response = result
        # 最后把一个结果给返回出去
        return allure_response

    # 写个方法 execute_test_cases--传一个参数 接口的信息--即写yaml文件信息
    def execute_test_cases(self, api_info):
        """
        规范yaml接口信息,执行接口、提取结果以及断言操作
        :param api_info:
        :return:
        """
        # 先打印查看api_info信息
        print(api_info)
        # 添加异常
        try:
            # 处理baseInfo里面的数据
            # 获取到接口的服务器地址--通过读取配置文件去获取--通过这个对象去调用
            conf_host = self.conf.get_host('host')
            # 获取url--完整的url是通过 服务器地址    加上  接口信息api_info
            url = conf_host + api_info['baseInfo']['url']
            allure.attach(url, '接口地址:', attachment_type=allure.attachment_type.TEXT)
            # 接口名称
            api_name = api_info['baseInfo']['api_name']
            allure.attach(api_name, f'接口名称:{api_name}', attachment_type=allure.attachment_type.TEXT)
            # 请求方式
            method = api_info['baseInfo']['method']
            allure.attach(method, f'请求方式:{method}', attachment_type=allure.attachment_type.TEXT)
            # 请求头--通过get方法获取看里面有没有请求头--如果有的话就返回给变量headers,若没有,则传个默认空值None就行
            # 判断请求头是否可选,使用get的话,当headers有的话就返回,没有时就返回None,不至于报错
            headers = api_info['baseInfo'].get('headers', None)
            # 再进行一步处理--判断 headers 不为空
            if headers is not None:
                # if isinstance(headers, str)    判断headers是什么类型?--如果是一个字符串str类型的话 就需要给它做一个解析
                # self.parse_and_replace_variables(headers)  调用parse_and_replace_variables方法,把headers给传进来
                # else headers   若不是字符串,直接给他返回headers
                # headers =    最后再把结果传回给它自己就行了
                # eval()    调用一个函数 把解析后的数据 self.parse_and_replace_variables(headers) 转换为 字典类型
                headers = eval(self.parse_and_replace_variables(headers)) if isinstance(headers, str) else headers
                # print(headers)
            allure.attach(json.dumps(headers), '请求头:', attachment_type=allure.attachment_type.JSON)

            # cookies 与 请求头一样
            cookies = api_info['baseInfo'].get('cookies', None)
            if cookies is not None:
                cookies = eval(self.parse_and_replace_variables(cookies)) if isinstance(cookies, str) else cookies
                # print(cookies)
            allure.attach(json.dumps(cookies), 'Cookies', attachment_type=allure.attachment_type.JSON)

            # 处理testCase里面的数据
            # 通过for循环--循环testcase--通过读取api_info--拿到最外层的数据testCase
            for testcase in api_info['testCase']:
                print(f'处理前:{testcase}')
                # 拿到之后,进行更近一步的处理--通过testcase.pop()去处理--先获取它第一组数据 case_name
                case_name = testcase.pop('case_name')
                allure.attach(case_name, f'测试用例:{case_name}', attachment_type=allure.attachment_type.TEXT)
                # 通过变量引用处理断言结果
                # 通过self.parse_and_replace_variables去断言解析的函数--通过for循环中testcase.get获取断言的关键词validation
                # 然后拿到validation后面的这组数据--再把断言的结果值给返回出去并赋值给变量val_result
                val_result = self.parse_and_replace_variables(testcase.get('validation'))
                # print(val_result)
                # 拿到断言结果,继续往下写--把testcase['validation']这个值更新成处理后的数据val_result
                testcase['validation'] = val_result
                # 同样是调用pop--然后去获取这个数据variables--最后把结果返回出去给variables
                validation = testcase.pop('validation')
                # print(validation, type(validation))
                # print(testcase, type(testcase))

                # 处理接口返回值提取部分
                # 通过testcase.pop去删除,删除之前再把结果返回,即返回再删除--获取extract关键词结果
                # --可以在跟一个参数,设置默认值,当login.yaml文件中有extract这个值的时候就返回,没有返回一个None,它就不会报错了
                # extract = testcase.pop('extract', None)
                # print(extract)
                # 处理为 列表的情况 extract_list--要把每一种情况都考虑进去
                # extract_list = testcase.pop('extract_list', None)
                # print(extract_list)
                # 把extract、extract_list 写在一行--看个人习惯,分开写或者一起都行
                extract, extract_list = testcase.pop('extract', None), testcase.pop('extract_list', None)
                print(f'处理后:{testcase}')

                # 处理参数类型和请求参数
                # 使用for循环--参数类型param_type、参数值param_value--它是一个字典直接通过testcase.items() 字典函数
                for param_type, param_value in testcase.items():
                    # 添加参数类型,使用attach调用第一个函数param_type就是要添加的内容,它的一个类型--第二个就是标题"参数类型"--第三个是内容的显示类型
                    allure.attach(param_type, '参数类型', attachment_type=allure.attachment_type.TEXT)
                    # 拿到之后开始进一步操作
                    # print(f'类型:{param_type}, 参数值:{param_value}')
                    # 先判断类型 param_type 在不在这三种其中之一--也就是说参数类型只能写这三种里面的一个
                    if param_type in ['params', 'data', 'json']:
                        # 若是在的话则可以解析了--直接去调用解析的方法--把参数值param_value传递给它--最后再把结果返回出去赋值给变量request_params
                        request_params = self.parse_and_replace_variables(param_value)
                        # print(request_params)
                        # 把testcase字典key值 参数类型更新为 通过解析之后的数据 request_params
                        testcase[param_type] = request_params
                        # 添加入参,第一个入参是解析替换过之后的request_params(字典类型返回时会报错,需要转换为字符串类型,所以跟响应结果一样)
                        allure.attach(self.allure_attach_dict_result(request_params), '请求参数',
                                      attachment_type=allure.attachment_type.JSON)
                print('处理之后:', testcase)

                # 文件上传处理
                files = testcase.pop('files', None)
                if files:
                    for fk, fv in files.items():
                        files = {fk: open(fv, mode='rb')}

                # 通过刚刚创建的实例化对象self.send_request去调用执行接口请求的方法execute_api_request()--里面的参数需要一一对应
                # **kwargs 未知数量传参------------直接把 **testcase 传给它
                response = self.send_request.execute_api_request(api_name=api_name, url=url, method=method, header=headers, case_name=case_name, cookie=cookies, file=files, **testcase)
                # 获取状态码和接口返回结果的text文本格式
                status_code, response_text = response.status_code, response.text
                print(f'接口实际返回结果:{response_text}')
                # 直接调用self.allure_attach_response()--把响应结果.json()--response.json()给它传进去
                allure.attach(self.allure_attach_dict_result(response.json()), '接口实际响应信息',
                              attachment_type=allure.attachment_type.JSON)

                # 处理接口返回值提取
                # 判断这个值extract不等于None的话
                if extract is not None:
                    # 就调用封装的方法extract_data--传递--第一个参数extract------------第二个参数response
                    self.extract_data(extract, response_text)
                # 判断这个值extract_list不等于None的话
                if extract_list is not None:
                    # 调用封装的方法extract_data_list--传递--第一个参数extract_list------------第二个参数response
                    self.extract_data_list(extract_list, response_text)

                # 处理接口断言
                self.asserts.assert_result(validation, response.json(), status_code)

        except Exception as e:
            # 因暂时未写日志模块,所以这里先打印出来
            # {str(traceback.format_exc())}--当出现异常时,会打印出比较详细的信息
            print(f'出现未知异常:--{str(traceback.format_exc())}')
            # 这里出现异常时,需手动抛出异常--在生成报告时,这里若没有抛出这个异常的话,在测试出现失败时,还是会统计为成功的
            raise e

    # 创建一个方法 extract_data 提取接口的返回值--接收两个参数,testcase_extract就是yaml文件中的token: $.token------------response_text 接口返回信息,文本格式
    @classmethod
    def extract_data(cls, testcase_extract, response_text):
        """
        提取单个参数,提取接口的返回参数,支持正则表达式提取和json提取器
        :param testcase_extract:    (dict)yaml文件中的extract值,例如:{'token':'$.token'}
        :param response_text:   (str)接口的实际返回值
        :return:
        """
        # 先打印一下表达式 {'token':'$.token'}
        # print(testcase_extract)
        extract_data = None
        try:
            # 使用for循环------------因为这个值token: $.token是一个字典类型,要分别去拿到要提取变量的名称token,还有后面拿到表达式$.token
            for key, value in testcase_extract.items():
                # any() 函数是 当里面的元素都为真True的情况下,就返回True------------则if通过,才会执行下面的语句------------这里其实是一个生成器表达式
                # 使用for循环里面的列表------------正则表达式(贪婪匹配、非贪婪匹配)
                # '(.*?)':只是匹配0次或多次; '(.+?)':只是匹配1次或多次;
                # r'(\d+)'、r'(\d*)':加 r 是防止被转义;\d 是获取数字;+ 加号是获取至少1次或多次;* 星号是至少0次
                if any(pat in value for pat in ['(.*?)', '(.+?)', r'(\d+)', r'(\d*)']):
                    # 使用正则表达式的模块调用search函数去全局的搜索--第一个参数是它的正则表达式value,第二个是从接口的返回信息中去提取response_text
                    ext_list = re.search(value, response_text)
                    # 把数据存储下来,存到字典中--对值value进行一个判断--判断提取到的数据是字符串还是整数类型
                    # 先去判断 if r'(\d+)' in value 有没有这个值,有的话把它转成整数类型的 int(ext_list.group(1))
                    # 若不是 (\d+) 则直接执行 else 后面的语句 ext_list.group(1)
                    extract_data = {key: int(ext_list.group(1)) if r'(\d+)' in value else ext_list.group(1)}
                    # print(f'提取到的结果:{extract_data}')
                # 判断有没有 $ 符号
                elif '$' in value:
                    # 引入模块 import jsonpath --调用jsonpath()去提取--因为json提取需要json格式,而response_text是字符串类型的
                    # 使用json.loads(response_text)转换成字典类型,它才能提取--第二个值value--它最外层是列表,所以需要索引[0]取值
                    extract_json = jsonpath.jsonpath(json.loads(response_text), value)[0]
                    # 拿到接口返回值token
                    print(extract_json)
                    # 先判断提取的结果是否存在--若存在,则定义一个字典存储起来;若不存在,则直接提示打印一句话
                    extract_data = {key: extract_json} if extract_json else {key: "未提取到数据,请检查接口返回信息或表达式!"}
                    # 提取到的结果
                    print(extract_data)
                # 判断extract_data是否存在
                if extract_data:
                    # 引入写入方法 write_yaml()--若存在,则调用方法--把结果字典类型的extract_data给存进去
                    write_yaml(extract_data)

        except re.error:
            print('正则表达式解析错误,请检查yaml文件extract表达式是否正确!')
        except json.JSONDecodeError:
            print('JSON解析错误,请检查yaml文件extract表达式是否正确!')

    @classmethod
    def extract_data_list(cls, testcase_extract_list, response_text):
        """
        提取多个参数,提取接口的返回参数,支持正则表达式提取和json提取器
        :param testcase_extract_list:    (dict)yaml文件中的extract_list值,例如:{'token':'$.token'}
        :param response_text:   (str)接口的实际返回值
        :return:
        """
        extract_data = None
        try:
            # 跟上面的方法一样,先去循环拿到key值、value值
            for key, value in testcase_extract_list.items():
                if any(pat in value for pat in ['(.*?)', '(.+?)', r'(\d+)', r'(\d*)']):
                    # 使用正则表达式的模块调用findall函数--传参还是一样的-- re.S --re模块的一个选项,表示在模式中匹配任何字符,包括换行符
                    ext_list = re.findall(value, response_text, re.S)
                    print(ext_list)
                    # 判断提取的结果对不对
                    if ext_list:
                        # 存储起来
                        extract_data = {key: ext_list}
                        print(extract_data)
                # json提取器
                elif "$" in value:
                    # 这里是提取多个,是一个列表,所以不需要通过索引[0]去取值
                    extract_json = jsonpath.jsonpath(json.loads(response_text), value)
                    print(extract_json)
                    # 判断提取的这个值是否存在
                    if extract_json:
                        extract_data = {key: extract_json}
                    else:
                        extract_data= {key: '未提取到数据,请检查接口返回信息或表达式!'}
                # 判断extract_data是否存在
                if extract_data:
                    # 写入方法
                    write_yaml(extract_data)

        except re.error:
            print('正则表达式解析错误,请检查yaml文件extract表达式是否正确!')
        except json.JSONDecodeError:
            print('JSON解析错误,请检查yaml文件extract表达式是否正确!')

# 测试调试查看
if __name__ == '__main__':
    # 先引进读取方法 read_yaml--传如一个它的相对路径--赋值给新变量data--[0]通过索引去列表值第一个
    api_info = read_yaml('../testcases/login/login.yaml')[0]
    # 实例化类 RequestsBase()--并赋值给一个变量对象 req
    req = RequestsBase()
    # 通过类对象去调用parse_and_replace_variables(data) 这个解析的方法--把读取到的 data 这个yaml数据传给它
    # res = req.parse_and_replace_variables(data)
    # print(f'解析后:{res}')
    # 通过这个对象req去调用刚刚封装的方法execute_test_cases--把读取到yaml文件的数据api_info传给它
    req.execute_test_cases(api_info)

24、重新运行项目根目录下主函数 run.py 文件,查看到 allure 测试报告右侧信息测试步骤里面中的"参数类型"已显示出数据信息;

五、优化冗余代码(可选)

可按需选择是否再进一步优化,此优化请根据自身实际结果,建议尽量优化,后期比较好维护,若不优化亦可;例如:像" allure.attach "很多重复代码块可继续优化,把要显示的标题和内容给做成一个字典的形式去映射,然后通过一个循环去分别拿到要显示的"标题和内容",再传递给它就行了;

25、修改"apiutils.py"文件,优化代码冗余;

复制代码
# 导包
import json
import jsonpath
from unit_tools.handle_data.yaml_handler import read_yaml, write_yaml
import re
from unit_tools.debugtalk import DebugTalk
# 引入 配置文件模块
from unit_tools.handle_data.configParse import ConfigParser
# 引入 发起请求模块
from unit_tools.sendrequests import SendRequests
# 引入 断言类
from unit_tools.assertion_utils import Assertions
# 导入模块
import traceback
import allure

# 创建一个类 RequestsBase
class RequestsBase:

    # 在RequestsBase类的里面写一个初始化构造函数
    def __init__(self):
        # 实例化一下,实例化之后就可以通过这个对象去获取 host
        self.conf = ConfigParser()
        # 实例化
        self.send_request = SendRequests()
        # 实例化断言类
        self.asserts = Assertions()

    # 创建一个方法 parse_and_replace_variables(解析并且替换变量)--传一个参数 yml_data--要解析的yaml文件的数组
    def parse_and_replace_variables(self, yml_data):
        """
        解析并替换YAML文件数据中的变量引用,如:${get_extract_data(goodsId,1)}
        :param yml_data: 解析的YAML数据
        :return: 最终要返回的是dict类型--才能给后续的一些方法调用时使用
        """
        # if isinstance(yml_data, str)--判断传进来的数据是什么类型?--后续需要字符串才能做操作的,所以这里先判断是不是字符串格式
        # yml_data若是字符串则执行 if 语句左边的代码 直接返回yml_data
        # yml_data不是字符串--调用json.dumps(yml_data)模块转成字符串--ensure_ascii=False 参数是可以处理中文
        # if else 语句写在一行是 三元运算--通过这一系列之后最终返回一个 新的变量yaml_data_str --它是一个字符串
        yml_data_str = yml_data if isinstance(yml_data, str) else json.dumps(yml_data, ensure_ascii=False)
        # 打印查看 yaml_data_str
        # print(f'解析前:{yml_data_str}')
        # 用for循环去判断 字符串yml_data_str里面这种标识 "${" 格式出现的次数--加一个'_'下划线表示不需要用到这个变量,是一个占位符的意思
        for _ in range(yml_data_str.count("${")):
            # if判断这个标识'${' 它包含在字符串yml_data_str里面--并且另一半它 '}' 也包含在字符串yml_data_str里面
            if '${' in yml_data_str and '}' in yml_data_str:
                # 通过获取到 "${" 标识的起始位置--通过字符串 yml_data_str 的索引 index--获取它的索引的起始位置
                strat_index = yml_data_str.index("${")
                # 获取结束位置 标识 "}"--再加一个参数 它的开头 strat_index
                end_index = yml_data_str.index("}", strat_index)
                # 拿到开始、结束位置后--通过字符串切片--起始位置、结束位置+1--为什么+1?--因为python中索引切片是左闭右开的一个规则
                # 什么是左闭右开?--开始位置是包含在切片里面的,但是结束元素不包含在切片中,所以得+1
                variable_data = yml_data_str[strat_index:end_index+1]

                # 使用正则表达式提取函数名和参数
                # 导入re模块--调用re模块中的match方法--里面跟两个参数,第一个参数是正则表达式,第二个参数是要提取正则表达式的字符串
                match = re.match(r'\$\{(\w+)\((.*?)\)\}', variable_data)
                # 判断一下有没有匹配到--匹配的对象是不是成功了--成功
                if match:
                    # 通过match.groups()去拿到函数名还有参数--定义两个变量去接收,第一个函数名func_name、第二个函数值func_params
                    func_name, func_params = match.groups()

                    # 先判断它存在--走if左边的代码--给它做一个切片;若参数值不存在,则返回一个空列表即可
                    # 参数值有一个或多个--所以需要做一个处理--参数值func_params通过逗号去做一个切片
                    # 最后再把值重新赋值给它自己
                    func_params = func_params.split(',') if func_params else []

                    # 使用面向对象反射getattr调用函数
                    # 引如类后直接 实例化类--第二个参数传的是一个函数名 func_name--函数调用 (*func_params)--通过星号把参数值func_params去解包传递给参数func_name
                    extract_data = getattr(DebugTalk(), func_name)(*func_params)
                    # print(f'提取到的结果:{extract_data}')
                    # 使用正则表达式替换原始字符串中的变量引用为调用后的结果
                    # 使用正则表达式替换函数re.sub--第一个参数就是需要替换的哪个参数值,第二个参数是解析后得到的数据extract_data,第三个参数是原始解析前的yml_data_str
                    yml_data_str = re.sub(re.escape(variable_data), str(extract_data), yml_data_str)
        # 还原数据,将其转换为字典类型
        # 添加异常处理--像这种转换的最好是加一个异常处理
        try:
            # 直接调用 son.loads(yml_data_str)--把字符串转换成字典
            data = json.loads(yml_data_str)
        # json转码异常
        except json.JSONDecodeError:
            # 返回原始数据yml_data_str--赋值给新变量 data
            data = yml_data_str
        return data

    # 写个类方法,因为它不需要访问实例属性
    @classmethod
    def allure_attach_dict_result(cls, result):
        """
        在allure测试报告中显示接口响应信息,让它展示JSON格式;处理结果是字典类型,就将其转化成字符串,并做格式化处理,否则直接返回
        :param response:  传一个参数,响应结果信息
        :return:
        """
        # 判断响应信息是什么类型?
        if isinstance(result, dict):
            # 如果是字典类型,通过json反序列化json.dumps()转成字符串--显示中文ensure_ascii=False--显示缩进 indent=4
            allure_response = json.dumps(result, ensure_ascii=False, indent=4)
        else:
            # 如果不是字典,直接把响应结果result赋值给它allure_response
            allure_response = result
        # 最后把一个结果给返回出去
        return allure_response

    # 写个方法 execute_test_cases--传一个参数 接口的信息--即写yaml文件信息
    def execute_test_cases(self, api_info):
        """
        规范yaml接口信息,执行接口、提取结果以及断言操作
        :param api_info:
        :return:
        """
        # 先打印查看api_info信息
        print(api_info)
        # 添加异常
        try:
            # 处理baseInfo里面的数据
            # 获取到接口的服务器地址--通过读取配置文件去获取--通过这个对象去调用
            conf_host = self.conf.get_host('host')
            # 获取url--完整的url是通过 服务器地址    加上  接口信息api_info
            url = conf_host + api_info['baseInfo']['url']
            allure.attach(url, '接口地址:', attachment_type=allure.attachment_type.TEXT)
            # 接口名称
            api_name = api_info['baseInfo']['api_name']
            allure.attach(api_name, f'接口名称:{api_name}', attachment_type=allure.attachment_type.TEXT)
            # 请求方式
            method = api_info['baseInfo']['method']
            allure.attach(method, f'请求方式:{method}', attachment_type=allure.attachment_type.TEXT)
            # 请求头--通过get方法获取看里面有没有请求头--如果有的话就返回给变量headers,若没有,则传个默认空值None就行
            # 判断请求头是否可选,使用get的话,当headers有的话就返回,没有时就返回None,不至于报错
            headers = api_info['baseInfo'].get('headers', None)
            # 再进行一步处理--判断 headers 不为空
            if headers is not None:
                # if isinstance(headers, str)    判断headers是什么类型?--如果是一个字符串str类型的话 就需要给它做一个解析
                # self.parse_and_replace_variables(headers)  调用parse_and_replace_variables方法,把headers给传进来
                # else headers   若不是字符串,直接给他返回headers
                # headers =    最后再把结果传回给它自己就行了
                # eval()    调用一个函数 把解析后的数据 self.parse_and_replace_variables(headers) 转换为 字典类型
                headers = eval(self.parse_and_replace_variables(headers)) if isinstance(headers, str) else headers
                # print(headers)
            allure.attach(json.dumps(headers), '请求头:', attachment_type=allure.attachment_type.JSON)

            # cookies 与 请求头一样
            cookies = api_info['baseInfo'].get('cookies', None)
            if cookies is not None:
                cookies = eval(self.parse_and_replace_variables(cookies)) if isinstance(cookies, str) else cookies
                # print(cookies)
            allure.attach(json.dumps(cookies), 'Cookies', attachment_type=allure.attachment_type.JSON)

            # 处理testCase里面的数据
            # 通过for循环--循环testcase--通过读取api_info--拿到最外层的数据testCase
            for testcase in api_info['testCase']:
                print(f'处理前:{testcase}')
                # 拿到之后,进行更近一步的处理--通过testcase.pop()去处理--先获取它第一组数据 case_name
                case_name = testcase.pop('case_name')
                allure.attach(case_name, f'测试用例:{case_name}', attachment_type=allure.attachment_type.TEXT)
                # 通过变量引用处理断言结果
                # 通过self.parse_and_replace_variables去断言解析的函数--通过for循环中testcase.get获取断言的关键词validation
                # 然后拿到validation后面的这组数据--再把断言的结果值给返回出去并赋值给变量val_result
                val_result = self.parse_and_replace_variables(testcase.get('validation'))
                # print(val_result)
                # 拿到断言结果,继续往下写--把testcase['validation']这个值更新成处理后的数据val_result
                testcase['validation'] = val_result
                # 同样是调用pop--然后去获取这个数据variables--最后把结果返回出去给variables
                validation = testcase.pop('validation')
                # print(validation, type(validation))
                # print(testcase, type(testcase))

                # 处理接口返回值提取部分
                # 通过testcase.pop去删除,删除之前再把结果返回,即返回再删除--获取extract关键词结果
                # --可以在跟一个参数,设置默认值,当login.yaml文件中有extract这个值的时候就返回,没有返回一个None,它就不会报错了
                # extract = testcase.pop('extract', None)
                # print(extract)
                # 处理为 列表的情况 extract_list--要把每一种情况都考虑进去
                # extract_list = testcase.pop('extract_list', None)
                # print(extract_list)
                # 把extract、extract_list 写在一行--看个人习惯,分开写或者一起都行
                extract, extract_list = testcase.pop('extract', None), testcase.pop('extract_list', None)
                print(f'处理后:{testcase}')

                # 改变作用域问题,在外层定义None
                param_type, request_params = None, None
                # 处理参数类型和请求参数
                # 使用for循环--参数类型param_type、参数值param_value--它是一个字典直接通过testcase.items() 字典函数
                for param_type, param_value in testcase.items():
                    # 添加参数类型,使用attach调用第一个函数param_type就是要添加的内容,它的一个类型--第二个就是标题"参数类型"--第三个是内容的显示类型
                    allure.attach(param_type, '参数类型', attachment_type=allure.attachment_type.TEXT)
                    # 拿到之后开始进一步操作
                    # print(f'类型:{param_type}, 参数值:{param_value}')
                    # 先判断类型 param_type 在不在这三种其中之一--也就是说参数类型只能写这三种里面的一个
                    if param_type in ['params', 'data', 'json']:
                        # 若是在的话则可以解析了--直接去调用解析的方法--把参数值param_value传递给它--最后再把结果返回出去赋值给变量request_params
                        request_params = self.parse_and_replace_variables(param_value)
                        # print(request_params)
                        # 把testcase字典key值 参数类型更新为 通过解析之后的数据 request_params
                        testcase[param_type] = request_params
                        # 添加入参,第一个入参是解析替换过之后的request_params(字典类型返回时会报错,需要转换为字符串类型,所以跟响应结果一样)
                        allure.attach(self.allure_attach_dict_result(request_params), '请求参数',
                                      attachment_type=allure.attachment_type.JSON)
                print('处理之后:', testcase)

                # 文件上传处理
                files = testcase.pop('files', None)
                if files:
                    for fk, fv in files.items():
                        files = {fk: open(fv, mode='rb')}

                # 通过刚刚创建的实例化对象self.send_request去调用执行接口请求的方法execute_api_request()--里面的参数需要一一对应
                # **kwargs 未知数量传参------------直接把 **testcase 传给它
                response = self.send_request.execute_api_request(api_name=api_name, url=url, method=method, header=headers, case_name=case_name, cookie=cookies, file=files, **testcase)
                # 获取状态码和接口返回结果的text文本格式
                status_code, response_text = response.status_code, response.text
                print(f'接口实际返回结果:{response_text}')
                # 直接调用self.allure_attach_response()--把响应结果.json()--response.json()给它传进去
                allure.attach(self.allure_attach_dict_result(response.json()), '接口实际响应信息',
                              attachment_type=allure.attachment_type.JSON)

                # allure报告Test body显示内容
                # 可以通过定义一个字典类型allure_info 来显示信息--通过 key,value 键值对的方式管理要显示的内容
                allure_info = {
                    '接口地址': url,
                    '接口名称': api_name,
                    '请求方式': method,
                    '请求头': self.allure_attach_dict_result(headers if headers else '无需请求头'),
                    'Cookies': self.allure_attach_dict_result(cookies if cookies else '无需Cookies'),
                    '测试用例名称': case_name,
                    '参数类型': param_type if param_type else '无需参数类型',
                    '请求参数': self.allure_attach_dict_result(
                        request_params if request_params else '无需请求参数入参'),
                    '接口实际响应信息': self.allure_attach_dict_result(response.json())
                }

                # 通过 for 循环方便拿到字典allure_info的 key,value 信息--循环标题 title 和内容 content 调用字典专用函数 .items():
                for title, content in allure_info.items():
                    # 通过 allure.attach 这个函数--第一个参数是要显示的内容 content --第二个参数是标题(即循环之后的 title)
                    # --第三个参数就是一个类型(这里类型统一显示JSON格式,若有别的需求可以再做判断,这里就不做判断了,直接显示JSON格式)
                    allure.attach(content, title, attachment_type=allure.attachment_type.JSON)

                # 处理接口返回值提取
                # 判断这个值extract不等于None的话
                if extract is not None:
                    # 就调用封装的方法extract_data--传递--第一个参数extract------------第二个参数response
                    self.extract_data(extract, response_text)
                # 判断这个值extract_list不等于None的话
                if extract_list is not None:
                    # 调用封装的方法extract_data_list--传递--第一个参数extract_list------------第二个参数response
                    self.extract_data_list(extract_list, response_text)

                # 处理接口断言
                self.asserts.assert_result(validation, response.json(), status_code)

        except Exception as e:
            # 因暂时未写日志模块,所以这里先打印出来
            # {str(traceback.format_exc())}--当出现异常时,会打印出比较详细的信息
            print(f'出现未知异常:--{str(traceback.format_exc())}')
            # 这里出现异常时,需手动抛出异常--在生成报告时,这里若没有抛出这个异常的话,在测试出现失败时,还是会统计为成功的
            raise e

    # 创建一个方法 extract_data 提取接口的返回值--接收两个参数,testcase_extract就是yaml文件中的token: $.token------------response_text 接口返回信息,文本格式
    @classmethod
    def extract_data(cls, testcase_extract, response_text):
        """
        提取单个参数,提取接口的返回参数,支持正则表达式提取和json提取器
        :param testcase_extract:    (dict)yaml文件中的extract值,例如:{'token':'$.token'}
        :param response_text:   (str)接口的实际返回值
        :return:
        """
        # 先打印一下表达式 {'token':'$.token'}
        # print(testcase_extract)
        extract_data = None
        try:
            # 使用for循环------------因为这个值token: $.token是一个字典类型,要分别去拿到要提取变量的名称token,还有后面拿到表达式$.token
            for key, value in testcase_extract.items():
                # any() 函数是 当里面的元素都为真True的情况下,就返回True------------则if通过,才会执行下面的语句------------这里其实是一个生成器表达式
                # 使用for循环里面的列表------------正则表达式(贪婪匹配、非贪婪匹配)
                # '(.*?)':只是匹配0次或多次; '(.+?)':只是匹配1次或多次;
                # r'(\d+)'、r'(\d*)':加 r 是防止被转义;\d 是获取数字;+ 加号是获取至少1次或多次;* 星号是至少0次
                if any(pat in value for pat in ['(.*?)', '(.+?)', r'(\d+)', r'(\d*)']):
                    # 使用正则表达式的模块调用search函数去全局的搜索--第一个参数是它的正则表达式value,第二个是从接口的返回信息中去提取response_text
                    ext_list = re.search(value, response_text)
                    # 把数据存储下来,存到字典中--对值value进行一个判断--判断提取到的数据是字符串还是整数类型
                    # 先去判断 if r'(\d+)' in value 有没有这个值,有的话把它转成整数类型的 int(ext_list.group(1))
                    # 若不是 (\d+) 则直接执行 else 后面的语句 ext_list.group(1)
                    extract_data = {key: int(ext_list.group(1)) if r'(\d+)' in value else ext_list.group(1)}
                    # print(f'提取到的结果:{extract_data}')
                # 判断有没有 $ 符号
                elif '$' in value:
                    # 引入模块 import jsonpath --调用jsonpath()去提取--因为json提取需要json格式,而response_text是字符串类型的
                    # 使用json.loads(response_text)转换成字典类型,它才能提取--第二个值value--它最外层是列表,所以需要索引[0]取值
                    extract_json = jsonpath.jsonpath(json.loads(response_text), value)[0]
                    # 拿到接口返回值token
                    print(extract_json)
                    # 先判断提取的结果是否存在--若存在,则定义一个字典存储起来;若不存在,则直接提示打印一句话
                    extract_data = {key: extract_json} if extract_json else {key: "未提取到数据,请检查接口返回信息或表达式!"}
                    # 提取到的结果
                    print(extract_data)
                # 判断extract_data是否存在
                if extract_data:
                    # 引入写入方法 write_yaml()--若存在,则调用方法--把结果字典类型的extract_data给存进去
                    write_yaml(extract_data)

        except re.error:
            print('正则表达式解析错误,请检查yaml文件extract表达式是否正确!')
        except json.JSONDecodeError:
            print('JSON解析错误,请检查yaml文件extract表达式是否正确!')

    @classmethod
    def extract_data_list(cls, testcase_extract_list, response_text):
        """
        提取多个参数,提取接口的返回参数,支持正则表达式提取和json提取器
        :param testcase_extract_list:    (dict)yaml文件中的extract_list值,例如:{'token':'$.token'}
        :param response_text:   (str)接口的实际返回值
        :return:
        """
        extract_data = None
        try:
            # 跟上面的方法一样,先去循环拿到key值、value值
            for key, value in testcase_extract_list.items():
                if any(pat in value for pat in ['(.*?)', '(.+?)', r'(\d+)', r'(\d*)']):
                    # 使用正则表达式的模块调用findall函数--传参还是一样的-- re.S --re模块的一个选项,表示在模式中匹配任何字符,包括换行符
                    ext_list = re.findall(value, response_text, re.S)
                    print(ext_list)
                    # 判断提取的结果对不对
                    if ext_list:
                        # 存储起来
                        extract_data = {key: ext_list}
                        print(extract_data)
                # json提取器
                elif "$" in value:
                    # 这里是提取多个,是一个列表,所以不需要通过索引[0]去取值
                    extract_json = jsonpath.jsonpath(json.loads(response_text), value)
                    print(extract_json)
                    # 判断提取的这个值是否存在
                    if extract_json:
                        extract_data = {key: extract_json}
                    else:
                        extract_data= {key: '未提取到数据,请检查接口返回信息或表达式!'}
                # 判断extract_data是否存在
                if extract_data:
                    # 写入方法
                    write_yaml(extract_data)

        except re.error:
            print('正则表达式解析错误,请检查yaml文件extract表达式是否正确!')
        except json.JSONDecodeError:
            print('JSON解析错误,请检查yaml文件extract表达式是否正确!')

# 测试调试查看
if __name__ == '__main__':
    # 先引进读取方法 read_yaml--传如一个它的相对路径--赋值给新变量data--[0]通过索引去列表值第一个
    api_info = read_yaml('../testcases/login/login.yaml')[0]
    # 实例化类 RequestsBase()--并赋值给一个变量对象 req
    req = RequestsBase()
    # 通过类对象去调用parse_and_replace_variables(data) 这个解析的方法--把读取到的 data 这个yaml数据传给它
    # res = req.parse_and_replace_variables(data)
    # print(f'解析后:{res}')
    # 通过这个对象req去调用刚刚封装的方法execute_test_cases--把读取到yaml文件的数据api_info传给它
    req.execute_test_cases(api_info)

26、重新运行项目根目录下主函数 run.py 文件,查看到 allure 测试报告右侧信息测试步骤里面的数据信息,可以看到会出现数据重复,因为很多重复的" allure.attach "重复代码块还没有注释或者删除;

27、继续修改"apiutils.py"文件,注释或直接删除 像" allure.attach "这样的重复(共 9 处)代码块;

注释九处" allure.attach "代码块后的"apiutils.py"文件;

复制代码
# 导包
import json
import jsonpath
from unit_tools.handle_data.yaml_handler import read_yaml, write_yaml
import re
from unit_tools.debugtalk import DebugTalk
# 引入 配置文件模块
from unit_tools.handle_data.configParse import ConfigParser
# 引入 发起请求模块
from unit_tools.sendrequests import SendRequests
# 引入 断言类
from unit_tools.assertion_utils import Assertions
# 导入模块
import traceback
import allure

# 创建一个类 RequestsBase
class RequestsBase:

    # 在RequestsBase类的里面写一个初始化构造函数
    def __init__(self):
        # 实例化一下,实例化之后就可以通过这个对象去获取 host
        self.conf = ConfigParser()
        # 实例化
        self.send_request = SendRequests()
        # 实例化断言类
        self.asserts = Assertions()

    # 创建一个方法 parse_and_replace_variables(解析并且替换变量)--传一个参数 yml_data--要解析的yaml文件的数组
    def parse_and_replace_variables(self, yml_data):
        """
        解析并替换YAML文件数据中的变量引用,如:${get_extract_data(goodsId,1)}
        :param yml_data: 解析的YAML数据
        :return: 最终要返回的是dict类型--才能给后续的一些方法调用时使用
        """
        # if isinstance(yml_data, str)--判断传进来的数据是什么类型?--后续需要字符串才能做操作的,所以这里先判断是不是字符串格式
        # yml_data若是字符串则执行 if 语句左边的代码 直接返回yml_data
        # yml_data不是字符串--调用json.dumps(yml_data)模块转成字符串--ensure_ascii=False 参数是可以处理中文
        # if else 语句写在一行是 三元运算--通过这一系列之后最终返回一个 新的变量yaml_data_str --它是一个字符串
        yml_data_str = yml_data if isinstance(yml_data, str) else json.dumps(yml_data, ensure_ascii=False)
        # 打印查看 yaml_data_str
        # print(f'解析前:{yml_data_str}')
        # 用for循环去判断 字符串yml_data_str里面这种标识 "${" 格式出现的次数--加一个'_'下划线表示不需要用到这个变量,是一个占位符的意思
        for _ in range(yml_data_str.count("${")):
            # if判断这个标识'${' 它包含在字符串yml_data_str里面--并且另一半它 '}' 也包含在字符串yml_data_str里面
            if '${' in yml_data_str and '}' in yml_data_str:
                # 通过获取到 "${" 标识的起始位置--通过字符串 yml_data_str 的索引 index--获取它的索引的起始位置
                strat_index = yml_data_str.index("${")
                # 获取结束位置 标识 "}"--再加一个参数 它的开头 strat_index
                end_index = yml_data_str.index("}", strat_index)
                # 拿到开始、结束位置后--通过字符串切片--起始位置、结束位置+1--为什么+1?--因为python中索引切片是左闭右开的一个规则
                # 什么是左闭右开?--开始位置是包含在切片里面的,但是结束元素不包含在切片中,所以得+1
                variable_data = yml_data_str[strat_index:end_index+1]

                # 使用正则表达式提取函数名和参数
                # 导入re模块--调用re模块中的match方法--里面跟两个参数,第一个参数是正则表达式,第二个参数是要提取正则表达式的字符串
                match = re.match(r'\$\{(\w+)\((.*?)\)\}', variable_data)
                # 判断一下有没有匹配到--匹配的对象是不是成功了--成功
                if match:
                    # 通过match.groups()去拿到函数名还有参数--定义两个变量去接收,第一个函数名func_name、第二个函数值func_params
                    func_name, func_params = match.groups()

                    # 先判断它存在--走if左边的代码--给它做一个切片;若参数值不存在,则返回一个空列表即可
                    # 参数值有一个或多个--所以需要做一个处理--参数值func_params通过逗号去做一个切片
                    # 最后再把值重新赋值给它自己
                    func_params = func_params.split(',') if func_params else []

                    # 使用面向对象反射getattr调用函数
                    # 引如类后直接 实例化类--第二个参数传的是一个函数名 func_name--函数调用 (*func_params)--通过星号把参数值func_params去解包传递给参数func_name
                    extract_data = getattr(DebugTalk(), func_name)(*func_params)
                    # print(f'提取到的结果:{extract_data}')
                    # 使用正则表达式替换原始字符串中的变量引用为调用后的结果
                    # 使用正则表达式替换函数re.sub--第一个参数就是需要替换的哪个参数值,第二个参数是解析后得到的数据extract_data,第三个参数是原始解析前的yml_data_str
                    yml_data_str = re.sub(re.escape(variable_data), str(extract_data), yml_data_str)
        # 还原数据,将其转换为字典类型
        # 添加异常处理--像这种转换的最好是加一个异常处理
        try:
            # 直接调用 son.loads(yml_data_str)--把字符串转换成字典
            data = json.loads(yml_data_str)
        # json转码异常
        except json.JSONDecodeError:
            # 返回原始数据yml_data_str--赋值给新变量 data
            data = yml_data_str
        return data

    # 写个类方法,因为它不需要访问实例属性
    @classmethod
    def allure_attach_dict_result(cls, result):
        """
        在allure测试报告中显示接口响应信息,让它展示JSON格式;处理结果是字典类型,就将其转化成字符串,并做格式化处理,否则直接返回
        :param response:  传一个参数,响应结果信息
        :return:
        """
        # 判断响应信息是什么类型?
        if isinstance(result, dict):
            # 如果是字典类型,通过json反序列化json.dumps()转成字符串--显示中文ensure_ascii=False--显示缩进 indent=4
            allure_response = json.dumps(result, ensure_ascii=False, indent=4)
        else:
            # 如果不是字典,直接把响应结果result赋值给它allure_response
            allure_response = result
        # 最后把一个结果给返回出去
        return allure_response

    # 写个方法 execute_test_cases--传一个参数 接口的信息--即写yaml文件信息
    def execute_test_cases(self, api_info):
        """
        规范yaml接口信息,执行接口、提取结果以及断言操作
        :param api_info:
        :return:
        """
        # 先打印查看api_info信息
        print(api_info)
        # 添加异常
        try:
            # 处理baseInfo里面的数据
            # 获取到接口的服务器地址--通过读取配置文件去获取--通过这个对象去调用
            conf_host = self.conf.get_host('host')
            # 获取url--完整的url是通过 服务器地址    加上  接口信息api_info
            url = conf_host + api_info['baseInfo']['url']
            # allure.attach(url, '接口地址:', attachment_type=allure.attachment_type.TEXT)
            # 接口名称
            api_name = api_info['baseInfo']['api_name']
            # allure.attach(api_name, f'接口名称:{api_name}', attachment_type=allure.attachment_type.TEXT)
            # 请求方式
            method = api_info['baseInfo']['method']
            # allure.attach(method, f'请求方式:{method}', attachment_type=allure.attachment_type.TEXT)
            # 请求头--通过get方法获取看里面有没有请求头--如果有的话就返回给变量headers,若没有,则传个默认空值None就行
            # 判断请求头是否可选,使用get的话,当headers有的话就返回,没有时就返回None,不至于报错
            headers = api_info['baseInfo'].get('headers', None)
            # 再进行一步处理--判断 headers 不为空
            if headers is not None:
                # if isinstance(headers, str)    判断headers是什么类型?--如果是一个字符串str类型的话 就需要给它做一个解析
                # self.parse_and_replace_variables(headers)  调用parse_and_replace_variables方法,把headers给传进来
                # else headers   若不是字符串,直接给他返回headers
                # headers =    最后再把结果传回给它自己就行了
                # eval()    调用一个函数 把解析后的数据 self.parse_and_replace_variables(headers) 转换为 字典类型
                headers = eval(self.parse_and_replace_variables(headers)) if isinstance(headers, str) else headers
                # print(headers)
            # allure.attach(json.dumps(headers), '请求头:', attachment_type=allure.attachment_type.JSON)

            # cookies 与 请求头一样
            cookies = api_info['baseInfo'].get('cookies', None)
            if cookies is not None:
                cookies = eval(self.parse_and_replace_variables(cookies)) if isinstance(cookies, str) else cookies
                # print(cookies)
            # allure.attach(json.dumps(cookies), 'Cookies', attachment_type=allure.attachment_type.JSON)

            # 处理testCase里面的数据
            # 通过for循环--循环testcase--通过读取api_info--拿到最外层的数据testCase
            for testcase in api_info['testCase']:
                print(f'处理前:{testcase}')
                # 拿到之后,进行更近一步的处理--通过testcase.pop()去处理--先获取它第一组数据 case_name
                case_name = testcase.pop('case_name')
                # allure.attach(case_name, f'测试用例:{case_name}', attachment_type=allure.attachment_type.TEXT)
                # 通过变量引用处理断言结果
                # 通过self.parse_and_replace_variables去断言解析的函数--通过for循环中testcase.get获取断言的关键词validation
                # 然后拿到validation后面的这组数据--再把断言的结果值给返回出去并赋值给变量val_result
                val_result = self.parse_and_replace_variables(testcase.get('validation'))
                # print(val_result)
                # 拿到断言结果,继续往下写--把testcase['validation']这个值更新成处理后的数据val_result
                testcase['validation'] = val_result
                # 同样是调用pop--然后去获取这个数据variables--最后把结果返回出去给variables
                validation = testcase.pop('validation')
                # print(validation, type(validation))
                # print(testcase, type(testcase))

                # 处理接口返回值提取部分
                # 通过testcase.pop去删除,删除之前再把结果返回,即返回再删除--获取extract关键词结果
                # --可以在跟一个参数,设置默认值,当login.yaml文件中有extract这个值的时候就返回,没有返回一个None,它就不会报错了
                # extract = testcase.pop('extract', None)
                # print(extract)
                # 处理为 列表的情况 extract_list--要把每一种情况都考虑进去
                # extract_list = testcase.pop('extract_list', None)
                # print(extract_list)
                # 把extract、extract_list 写在一行--看个人习惯,分开写或者一起都行
                extract, extract_list = testcase.pop('extract', None), testcase.pop('extract_list', None)
                print(f'处理后:{testcase}')

                # 改变作用域问题,在外层定义None
                param_type, request_params = None, None
                # 处理参数类型和请求参数
                # 使用for循环--参数类型param_type、参数值param_value--它是一个字典直接通过testcase.items() 字典函数
                for param_type, param_value in testcase.items():
                    # 添加参数类型,使用attach调用第一个函数param_type就是要添加的内容,它的一个类型--第二个就是标题"参数类型"--第三个是内容的显示类型
                    # allure.attach(param_type, '参数类型', attachment_type=allure.attachment_type.TEXT)
                    # 拿到之后开始进一步操作
                    # print(f'类型:{param_type}, 参数值:{param_value}')
                    # 先判断类型 param_type 在不在这三种其中之一--也就是说参数类型只能写这三种里面的一个
                    if param_type in ['params', 'data', 'json']:
                        # 若是在的话则可以解析了--直接去调用解析的方法--把参数值param_value传递给它--最后再把结果返回出去赋值给变量request_params
                        request_params = self.parse_and_replace_variables(param_value)
                        # print(request_params)
                        # 把testcase字典key值 参数类型更新为 通过解析之后的数据 request_params
                        testcase[param_type] = request_params
                        # 添加入参,第一个入参是解析替换过之后的request_params(字典类型返回时会报错,需要转换为字符串类型,所以跟响应结果一样)
                        # allure.attach(self.allure_attach_dict_result(request_params), '请求参数',
                                      # attachment_type=allure.attachment_type.JSON)
                print('处理之后:', testcase)

                # 文件上传处理
                files = testcase.pop('files', None)
                if files:
                    for fk, fv in files.items():
                        files = {fk: open(fv, mode='rb')}

                # 通过刚刚创建的实例化对象self.send_request去调用执行接口请求的方法execute_api_request()--里面的参数需要一一对应
                # **kwargs 未知数量传参------------直接把 **testcase 传给它
                response = self.send_request.execute_api_request(api_name=api_name, url=url, method=method, header=headers, case_name=case_name, cookie=cookies, file=files, **testcase)
                # 获取状态码和接口返回结果的text文本格式
                status_code, response_text = response.status_code, response.text
                print(f'接口实际返回结果:{response_text}')
                # 直接调用self.allure_attach_response()--把响应结果.json()--response.json()给它传进去
                # allure.attach(self.allure_attach_dict_result(response.json()), '接口实际响应信息',
                #               attachment_type=allure.attachment_type.JSON)

                # allure报告Test body显示内容
                # 可以通过定义一个字典类型allure_info 来显示信息--通过 key,value 键值对的方式管理要显示的内容
                allure_info = {
                    '接口地址': url,
                    '接口名称': api_name,
                    '请求方式': method,
                    '请求头': self.allure_attach_dict_result(headers if headers else '无需请求头'),
                    'Cookies': self.allure_attach_dict_result(cookies if cookies else '无需Cookies'),
                    '测试用例名称': case_name,
                    '参数类型': param_type if param_type else '无需参数类型',
                    '请求参数': self.allure_attach_dict_result(
                        request_params if request_params else '无需请求参数入参'),
                    '接口实际响应信息': self.allure_attach_dict_result(response.json())
                }

                # 通过 for 循环方便拿到字典allure_info的 key,value 信息--循环标题 title 和内容 content 调用字典专用函数 .items():
                for title, content in allure_info.items():
                    # 通过 allure.attach 这个函数--第一个参数是要显示的内容 content --第二个参数是标题(即循环之后的 title)
                    # --第三个参数就是一个类型(这里类型统一显示JSON格式,若有别的需求可以再做判断,这里就不做判断了,直接显示JSON格式)
                    allure.attach(content, title, attachment_type=allure.attachment_type.JSON)

                # 处理接口返回值提取
                # 判断这个值extract不等于None的话
                if extract is not None:
                    # 就调用封装的方法extract_data--传递--第一个参数extract------------第二个参数response
                    self.extract_data(extract, response_text)
                # 判断这个值extract_list不等于None的话
                if extract_list is not None:
                    # 调用封装的方法extract_data_list--传递--第一个参数extract_list------------第二个参数response
                    self.extract_data_list(extract_list, response_text)

                # 处理接口断言
                self.asserts.assert_result(validation, response.json(), status_code)

        except Exception as e:
            # 因暂时未写日志模块,所以这里先打印出来
            # {str(traceback.format_exc())}--当出现异常时,会打印出比较详细的信息
            print(f'出现未知异常:--{str(traceback.format_exc())}')
            # 这里出现异常时,需手动抛出异常--在生成报告时,这里若没有抛出这个异常的话,在测试出现失败时,还是会统计为成功的
            raise e

    # 创建一个方法 extract_data 提取接口的返回值--接收两个参数,testcase_extract就是yaml文件中的token: $.token------------response_text 接口返回信息,文本格式
    @classmethod
    def extract_data(cls, testcase_extract, response_text):
        """
        提取单个参数,提取接口的返回参数,支持正则表达式提取和json提取器
        :param testcase_extract:    (dict)yaml文件中的extract值,例如:{'token':'$.token'}
        :param response_text:   (str)接口的实际返回值
        :return:
        """
        # 先打印一下表达式 {'token':'$.token'}
        # print(testcase_extract)
        extract_data = None
        try:
            # 使用for循环------------因为这个值token: $.token是一个字典类型,要分别去拿到要提取变量的名称token,还有后面拿到表达式$.token
            for key, value in testcase_extract.items():
                # any() 函数是 当里面的元素都为真True的情况下,就返回True------------则if通过,才会执行下面的语句------------这里其实是一个生成器表达式
                # 使用for循环里面的列表------------正则表达式(贪婪匹配、非贪婪匹配)
                # '(.*?)':只是匹配0次或多次; '(.+?)':只是匹配1次或多次;
                # r'(\d+)'、r'(\d*)':加 r 是防止被转义;\d 是获取数字;+ 加号是获取至少1次或多次;* 星号是至少0次
                if any(pat in value for pat in ['(.*?)', '(.+?)', r'(\d+)', r'(\d*)']):
                    # 使用正则表达式的模块调用search函数去全局的搜索--第一个参数是它的正则表达式value,第二个是从接口的返回信息中去提取response_text
                    ext_list = re.search(value, response_text)
                    # 把数据存储下来,存到字典中--对值value进行一个判断--判断提取到的数据是字符串还是整数类型
                    # 先去判断 if r'(\d+)' in value 有没有这个值,有的话把它转成整数类型的 int(ext_list.group(1))
                    # 若不是 (\d+) 则直接执行 else 后面的语句 ext_list.group(1)
                    extract_data = {key: int(ext_list.group(1)) if r'(\d+)' in value else ext_list.group(1)}
                    # print(f'提取到的结果:{extract_data}')
                # 判断有没有 $ 符号
                elif '$' in value:
                    # 引入模块 import jsonpath --调用jsonpath()去提取--因为json提取需要json格式,而response_text是字符串类型的
                    # 使用json.loads(response_text)转换成字典类型,它才能提取--第二个值value--它最外层是列表,所以需要索引[0]取值
                    extract_json = jsonpath.jsonpath(json.loads(response_text), value)[0]
                    # 拿到接口返回值token
                    print(extract_json)
                    # 先判断提取的结果是否存在--若存在,则定义一个字典存储起来;若不存在,则直接提示打印一句话
                    extract_data = {key: extract_json} if extract_json else {key: "未提取到数据,请检查接口返回信息或表达式!"}
                    # 提取到的结果
                    print(extract_data)
                # 判断extract_data是否存在
                if extract_data:
                    # 引入写入方法 write_yaml()--若存在,则调用方法--把结果字典类型的extract_data给存进去
                    write_yaml(extract_data)

        except re.error:
            print('正则表达式解析错误,请检查yaml文件extract表达式是否正确!')
        except json.JSONDecodeError:
            print('JSON解析错误,请检查yaml文件extract表达式是否正确!')

    @classmethod
    def extract_data_list(cls, testcase_extract_list, response_text):
        """
        提取多个参数,提取接口的返回参数,支持正则表达式提取和json提取器
        :param testcase_extract_list:    (dict)yaml文件中的extract_list值,例如:{'token':'$.token'}
        :param response_text:   (str)接口的实际返回值
        :return:
        """
        extract_data = None
        try:
            # 跟上面的方法一样,先去循环拿到key值、value值
            for key, value in testcase_extract_list.items():
                if any(pat in value for pat in ['(.*?)', '(.+?)', r'(\d+)', r'(\d*)']):
                    # 使用正则表达式的模块调用findall函数--传参还是一样的-- re.S --re模块的一个选项,表示在模式中匹配任何字符,包括换行符
                    ext_list = re.findall(value, response_text, re.S)
                    print(ext_list)
                    # 判断提取的结果对不对
                    if ext_list:
                        # 存储起来
                        extract_data = {key: ext_list}
                        print(extract_data)
                # json提取器
                elif "$" in value:
                    # 这里是提取多个,是一个列表,所以不需要通过索引[0]去取值
                    extract_json = jsonpath.jsonpath(json.loads(response_text), value)
                    print(extract_json)
                    # 判断提取的这个值是否存在
                    if extract_json:
                        extract_data = {key: extract_json}
                    else:
                        extract_data= {key: '未提取到数据,请检查接口返回信息或表达式!'}
                # 判断extract_data是否存在
                if extract_data:
                    # 写入方法
                    write_yaml(extract_data)

        except re.error:
            print('正则表达式解析错误,请检查yaml文件extract表达式是否正确!')
        except json.JSONDecodeError:
            print('JSON解析错误,请检查yaml文件extract表达式是否正确!')

# 测试调试查看
if __name__ == '__main__':
    # 先引进读取方法 read_yaml--传如一个它的相对路径--赋值给新变量data--[0]通过索引去列表值第一个
    api_info = read_yaml('../testcases/login/login.yaml')[0]
    # 实例化类 RequestsBase()--并赋值给一个变量对象 req
    req = RequestsBase()
    # 通过类对象去调用parse_and_replace_variables(data) 这个解析的方法--把读取到的 data 这个yaml数据传给它
    # res = req.parse_and_replace_variables(data)
    # print(f'解析后:{res}')
    # 通过这个对象req去调用刚刚封装的方法execute_test_cases--把读取到yaml文件的数据api_info传给它
    req.execute_test_cases(api_info)

需要注意,注释或直接删除九处" allure.attach "重复代码块后,第十处也就是最后一个需要保留,否则程序运行错误;

28、重新运行项目根目录下主函数 run.py 文件,查看到 allure 测试报告信息已准确显示数据;

建议可直接参考学习:使用 Python 语言 从 0 到 1 搭建完整 Web UI自动化测试学习系列 42--添加allure测试报告显示信息

在 allure 测试报告中显示测试环境信息

需要去创建一个文件,在生成测试报告时会先读取文件配置信息,然后把环境信息给显示到测试报告里面去;

29、在项目根目录下创建一个名称为"environment.xml"的文件,自定义输入以下内容;

复制代码
<environment>   <!-- 根节点-这是一段注释 -->
    <parameter>     <!-- 子节点 -->
        <key>system</key>
        <value>Windows 11</value>
    </parameter>
    <parameter>
        <key>Python version</key>
        <value>python 3.12.3</value>
    </parameter>
    <parameter>
        <key>allure version</key>
        <value>allure-pytest 2.13.5</value>
    </parameter>
    <parameter>
        <key>项目名</key>
        <value>某某XXXX某某</value>
    </parameter>
    <parameter>
        <key>作者</key>
        <value>XXXX</value>
    </parameter>
    <parameter>
        <key>日期</key>
        <value>XXXX</value>
    </parameter>
</environment>

因为之前在 pytest.ini 配置文件中写了一个清空的方法"--clean-alluredir"所以不能手动把 environment.xml复制到临时文件 temp 中去,因为每次执行的时候会先把 temp 文件先清空掉,再去重新生成;若手动把 environment.xml 复制到 temp 里面去,会找不到,因为每次会先清除,所以需要通过一个模块每次手动去复制;

30、修改项目根目录下的主函数 run.py 文件内容;

复制代码
import pytest
import os
# 引入调用python中里面的模块 shutil
import shutil

if __name__ == '__main__':
    pytest.main()
    # 使用 shutil.copy 方法手动复制--第一个参数是要复制的文件(相对路径 ./environment.xml)--第二个参数是要复制到哪里去的目录
    shutil.copy('./environment.xml', './report/temp')
    os.system('allure serve ./report/temp')

31、重新运行项目根目录下主函数 run.py 文件,可以查看到 allure 测试报告已成功显示出测试环境相关设置信息;

参考学习:使用 Python 语言 从 0 到 1 搭建完整 Web UI自动化测试学习系列 42--添加allure测试报告显示信息

控制 allure 测试报告测试用例执行顺序

后续介绍。

未完待续。。。

相关推荐
qetfw5 小时前
MXU:Tauri 2 + React 的 MaaFramework 跨平台 GUI 源码
前端·python·react.js·前端框架·开源项目·效率工具
用户8356290780516 小时前
Python 实现 Excel 页面布局与打印设置自动化
后端·python
一次旅行6 小时前
Python+大模型端到端自动化日报系统
开发语言·python·自动化
_Jimmy_7 小时前
AI应用开发工程师面试题库
人工智能·python·深度学习·机器学习·知识图谱
用户8356290780517 小时前
Python 实现 Excel 命名范围(Named Range)的创建与管理
后端·python
你想知道什么?7 小时前
线性回归-学习笔记
笔记·学习·线性回归
鱼毓屿御7 小时前
从「只会聊」到「边想边做」的跃迁
前端·学习·react.js
dreamer_83997 小时前
AI智能合同比对系统:从零搭建实战教程
人工智能·python
AOwhisky8 小时前
Python 学习笔记(第十五期)——运维自动化(下·后篇):堡垒机实战——paramiko高阶篇
运维·python·学习·云原生·自动化·运维开发