12 接口自动化-框架封装之 YAML 用例封装和热加载以及断言封装


接着 上一篇文章,继续进行框架封装

一、框架封装规则总结

接口自动化测试框架 yaml 用例编写规则:

1、必须有的四个一级关键字:name,base_url,request,validate
2、在 request 一级关键字下必须包括两个二级关键字:method,url
3、传参方式:在 request 一级关键字下,通过二级关键字传参:
    若是 get 请求,通过 params 关键字传参
    若是 post 请求:
        传 json 格式,通过 json 关键字传参
        传表单格式,通过 data 关键字传参
        传文件格式,通过 files 关键字传参
4、如果需要接口关联的话,必须使用一级关键字:extract
    提取值:
        如:json 提取方式
            extract:
              access_token: access_token
        如:正则表达式提取方式
            extract:
            access_token: '"access_token":"(.*?)"'
    取值:
        如:access_token: "{{access_token}}"
5、热加载,当 yaml 需要使用动态参数时,那么可以在 debug_talk.py 文件中写方法调用
    注意:传参时,需要什么类型的数据,需要做强转。 int(min),int(max)
    如:
        # 获取随机数的方法
        def get_random_number(self,start,stop):
            name = random.randrange(int(start),int(stop))
            return name
6、断言,以数组的方式编写每个断言
    如:
        validate:
        -   equals: {"status_code": 200}
        -   contains: url

二、框架代码简单实现

yaml_util.py - 处理 yaml 数据
import os
import yaml

# 获取项目根路径
def get_object_path():
    return os.getcwd().split('common')[0]

# 读取 config.yml 文件
def read_config_yaml(first_node,second_node):
    with open(get_object_path()+'/config.yml', 'r', encoding='utf-8') as f:
        yaml_config = yaml.load(f, Loader=yaml.FullLoader)
        return yaml_config[first_node][second_node]

# 读取 extract.yml 文件
def read_extract_yaml(first_node):
    with open(get_object_path()+'/extract.yml', 'r', encoding='utf-8') as f:
        yaml_config = yaml.load(f, Loader=yaml.FullLoader)
        return yaml_config[first_node]

# 写入 extract.yml 文件
def write_extract_yaml(data):
    with open(get_object_path()+'/extract.yml', 'a', encoding='utf-8') as f:
        yaml.dump(data, f,allow_unicode=True)

# 清空 extract.yml 文件
def clear_extract_yaml():
    with open(get_object_path()+'/extract.yml', 'w', encoding='utf-8') as f:
        f.truncate()

# 读取 YAML 测试用例文件
def read_testcase_yaml(yaml_path):
    with open(get_object_path()+yaml_path, 'r', encoding='utf-8') as f:
        yaml_value = yaml.load(f, Loader=yaml.FullLoader)
        return yaml_value
debug_talk.py - 热加载函数
import random
import time

class DebugTalk:
    # 获取随机数的方法
    def get_random_number(self, start, stop):
        name = random.randrange(int(start), int(stop))
        return name
    # 获取时间戳的方法
    def get_timestamp_str(self):
        name = time.strftime("_%Y%m%d_%H%M%S")
        return name
requests_util.py - 将请求封装在同一个方法中
import json
import re
import jsonpath
import requests
from common.yaml_util import read_extract_yaml, write_extract_yaml
from debug_talk import DebugTalk

class RequestsUtil:
    # 创建 session 会话对象
    session = requests.Session()

    # 寻找字典中某个 key 的值
    def find_key(self,key,data):
        # 处理字典类型(对象)
        if isinstance(data, dict):
            if key in data:
                return data[key]
            # 递归遍历所有值
            for value in data.values():
                result = self.find_key(key,value)
                if result is not None:
                    return result
        # 其他类型(如字符串、数字、数组等)直接跳过
        return None

    # 规范功能测试 YAML 测试用例文件的写法
    def analysis_yaml(self, case_info):
        # 1、必须有的四个一级关键字:name,base_url,request,validate
        case_info_keys = dict(case_info).keys()
        if 'name' in case_info_keys and 'base_url' in case_info_keys and 'request' in case_info_keys and 'validate' in case_info_keys:
            # 2、在 request 一级关键字下必须包括两个二级关键字:method,url
            request_keys = dict(case_info['request']).keys()
            if 'method' in request_keys and 'url' in request_keys:
                method = case_info['request']['method']
                url = case_info['base_url'] + case_info['request']['url']
                # 应该把method、url、headers、files四个参数从case_info['request']去掉后,剩下的数据传给kwargs
                case_info['request'].pop('method')
                case_info['request'].pop('url')
                # 参数(params、data、json),请求头,文件上传这些都不能做约束
                headers = None
                if jsonpath.jsonpath(case_info, '$..headers') :
                    headers = case_info['request']['headers']
                    case_info['request'].pop('headers')
                files = None
                if jsonpath.jsonpath(case_info, '$..files'):
                    files = case_info['request']['files']
                    for key, value in dict(files).items():
                        files[key] = open(value,"rb")
                    case_info['request'].pop('files')
                res = self.send_request(method, url, headers=headers, files=files, **case_info['request'])
                # 提取接口关联的变量,既要支持正则表达式也要支持json提取
                if 'extract' in case_info_keys:
                    for key, value in dict(case_info['extract']).items():
                        if '(.+?)' in value or '(.*?)' in value: # 正则表达式提取
                            re_value = re.search(value, res.text).group(1)
                            if re_value:
                                extract_data = {key: re_value}
                                write_extract_yaml(extract_data)
                        else: # json 提取
                            # 把中间变量写入 extract.yml 文件
                            extract_value = self.find_key(value,res.json())
                            if extract_value:
                                extract_data = {key: extract_value}
                                write_extract_yaml(extract_data)
                # 断言的封装调用
                self.validate_result(case_info['validate'], res.json(), res.status_code)
                return res
            else:
                print('2、在 request 一级关键字下必须包括两个二级关键字:method,url')
        else:
            print('1、必须有的四个一级关键字:name,base_url,request,validate')


    # 统一替换的方法,data 可以是 url(string),也可以是参数(字典,字典中包含列表),也可以是请求头(字典)
    # 比如把 access_token: "{{access_token}}" 中的{{access_token}} 替换成 真实的值
    def replace_value(self,data_value):
        # 不管是什么类型统一转换成字符串格式
        if data_value and isinstance(data_value, dict):  # 如果 data 不为空且 data 类型是一个字典
            str_data = json.dumps(data_value)
        else:
            str_data = data_value
        # 替换值
        for a in range(1, str_data.count('{{')+1):
            if '{{' in str_data and '}}' in str_data:
                start_index = str_data.index('{{')
                end_index = str_data.index('}}')
                old_value = str_data[start_index:end_index+2]
                new_value = read_extract_yaml(old_value[2:-2])
                str_data = str_data.replace(old_value, str(new_value))
        # 还原数据类型
        if data_value and isinstance(data_value, dict):  # 如果 data 不为空且 data类型是一个字典
            data_value = json.loads(str_data)
        else:
            data_value = str_data
        return data_value

    # 热加载替换解析
    # 比如{"name": "hc${get_random_number(10000,99999)}"} 会执行函数 getattr(self,"get_random_number")(10000,99999)
    def replace_load(self, data_value):
        # 不管是什么类型统一转换成字符串格式
        if data_value and isinstance(data_value, dict):  # 如果 data 不为空且 data 类型是一个字典
            str_data = json.dumps(data_value)
        else:
            str_data = data_value
        # 替换 ${函数调用} 为函数调用返回的值
        for a in range(1, str_data.count('${') + 1):
            if '${' in str_data and '}' in str_data:
                start_index = str_data.index('${')
                end_index = str_data.index('}')
                old_value = str_data[start_index:end_index + 1]
                func_name = old_value[2:old_value.index('(')]
                args_value = old_value[old_value.index('(')+1:old_value.index(')')]
                # 反射(通过一个函数的字符串直接去调用这个方法)
                if args_value:
                    new_value = getattr(DebugTalk(), func_name)(*args_value.split(','))
                else:
                    new_value = getattr(DebugTalk(), func_name)()
                str_data = str_data.replace(old_value, str(new_value))
        # 还原数据类型
        if data_value and isinstance(data_value, dict):  # 如果 data 不为空且 data 类型是一个字典
            data_value = json.loads(str_data)
        else:
            data_value = str_data
        return data_value

    # 统一发送请求的方法:
    def send_request(self, method, url, headers=None, **kwargs):
       # 处理 method 统一为小写
       lower_method = str(method).lower()
       # 处理基础路径
       # base_url = read_config_yaml("url", "base")
       # second_url = self.replace_value(read_config_yaml("url", url))
       # 处理请求头
       if headers:
           headers = self.replace_value(headers)
       # 最核心的地方:请求数据如何去替换,可能是 params、data、json
       for k, v in kwargs.items():
           if k in ['params', 'data', 'json']:
               value = self.replace_value(v)
               result_value = self.replace_load(value)
               kwargs[k] = result_value
       # 发送请求
       res = RequestsUtil.session.request(method=lower_method,url=url,headers=headers,**kwargs)
       return res

    # 断言封装
    def validate_result(self,expect_result,real_result,status_code):
        print("预期结果:"+str(expect_result))
        print("实际结果:"+str(real_result))
        # 断言的标记,flag = 0 断言成功,flag 不等于 0 断言失败
        flag = 0
        # 解析
        if expect_result and isinstance(expect_result,list):
            for expect_value in expect_result:
                for key, value in dict(expect_value).items():
                    if key == "equals":
                        for assert_key,assert_value in dict(value).items():
                            if assert_key == "status_code":
                                if status_code == assert_value:
                                    print("状态码断言成功")
                                else:
                                    flag = flag + 1
                                    print("状态码断言失败:" + assert_key + "不等于"+ assert_value)
                            else:
                                key_list = jsonpath.jsonpath(real_result,"$..%s"%assert_key)
                                if key_list:
                                    if assert_value not in key_list:
                                        flag = flag + 1
                                        print("断言失败:" + assert_key + "不等于" + assert_value)
                                    else:
                                        print(assert_key+"-断言成功")
                                else:
                                    flag = flag + 1
                                    print("断言失败:返回的结果中不存在" + assert_key)
                    elif key == "contains":
                       if value not in json.dumps(real_result):
                           flag = flag + 1
                           print("断言失败:返回的结果中不包含" + value)
                       else:
                           print(value + "-断言成功")
                    else:
                        print("框架不支持断言封装")

        assert flag==0,"断言失败!"
get_token.yml 文件
 -  name: get correct user token
    base_url: https://api.weixin.qq.com
    request:
      url: /cgi-bin/token
      method: GET
      params:
        appid: wxcb292044d4fdfd11
        secret: 69be902b0619de6bf75af4b0b9992645
        grant_type: client_credential
    validate:
    -   equals: {"status_code": 200}
    -   equals: {"expires_in": 7200}
    -   contains: access_token
    extract:
      access_token: access_token
#      access_token: '"access_token":"(.*?)"'
create_tag.yml 文件
 -  name: Create the user's tag
    base_url: https://api.weixin.qq.com
    request:
      url: /cgi-bin/tags/create
      method: POST
      params:
        access_token: "{{access_token}}"
      json: {"tag": {"name": "hc_create${get_random_number(10000,99999)}"}}
    validate:
    -   equals: {"status_code": 200}
    -   contains: tag
    extract:
      tag_id: id
update_tag.yml 文件
 -  name: Update the user's tag
    base_url: https://api.weixin.qq.com
    request:
      url: /cgi-bin/tags/update
      method: POST
      params:
        access_token: "{{access_token}}"
      json: {"tag": {"id": "{{tag_id}}","name": "hc_update${get_timestamp_str()}"}}
    validate:
    -   equals: {"status_code": 200}
    -   equals: {"errcode": 0}
    -   equals: {"errmsg": "ok"}
get_tag.yml 文件
 -  name: Get user's tags
    base_url: https://api.weixin.qq.com
    request:
      url: /cgi-bin/tags/get
      method: GET
      params:
        access_token: "{{access_token}}"
    validate:
    -   equals: {"status_code": 200}
    -   contains: tags
upload_file.yml 文件
 -  name: Upload image file
    base_url: https://api.weixin.qq.com
    request:
      url: /cgi-bin/media/uploadimg
      method: POST
      params:
        access_token: "{{access_token}}"
      files:
        media: "F:/Pycharm/TestAPI/screenshots/logo.png"
    validate:
    -   equals: {"status_code": 200}
    -   contains: url
test_tag.py - 测试用例执行
# -*- coding: utf-8 -*-
import json

import allure
import pytest
import requests
import yaml
from common.requests_util import RequestsUtil
from common.yaml_util import read_testcase_yaml, write_extract_yaml

@allure.epic("项目名称:接口自动化测试")
@allure.feature("模块名称:用户模块")
class TestUser:
    @allure.story("接口名称:获取用户token")
    @allure.severity(allure.severity_level.BLOCKER)
    @pytest.mark.user
    @pytest.mark.parametrize("case_info",read_testcase_yaml("/testcases/get_token.yml"))
    def test_get_token(self,case_info):
        res = RequestsUtil().analysis_yaml(case_info)
        allure.attach(body=str(res.status_code) + res.text,name="响应数据:",attachment_type=allure.attachment_type.TEXT)

    @allure.story("接口名称:创建用户标签")
    @allure.severity(allure.severity_level.NORMAL)
    @pytest.mark.user
    @pytest.mark.parametrize("case_info", read_testcase_yaml("/testcases/create_tag.yml"))
    def test_create_tag(self, case_info):
        res = RequestsUtil().analysis_yaml(case_info)
        allure.attach(body=str(res.status_code) + res.text, name="响应数据:",attachment_type=allure.attachment_type.TEXT)

    @allure.story("接口名称:更新用户标签")
    @allure.severity(allure.severity_level.NORMAL)
    @pytest.mark.user
    @pytest.mark.parametrize("case_info",read_testcase_yaml("/testcases/update_tag.yml"))
    def test_update_tag(self,case_info):
        res = RequestsUtil().analysis_yaml(case_info)
        allure.attach(body=str(res.status_code) + res.text, name="响应数据:",attachment_type=allure.attachment_type.TEXT)

    @allure.story("接口名称:获取用户标签")
    @allure.severity(allure.severity_level.NORMAL)
    @pytest.mark.user
    @pytest.mark.parametrize("case_info", read_testcase_yaml("/testcases/get_tag.yml"))
    def test_get_tag(self, case_info):
        res = RequestsUtil().analysis_yaml(case_info)
        allure.attach(body=str(res.status_code) + res.text, name="响应数据:",attachment_type=allure.attachment_type.TEXT)

    @allure.story("接口名称:文件上传")
    @allure.severity(allure.severity_level.NORMAL)
    @pytest.mark.user
    @pytest.mark.parametrize("case_info", read_testcase_yaml("/testcases/upload_file.yml"))
    def test_upload_file(self, case_info):
        res = RequestsUtil().analysis_yaml(case_info)
        allure.attach(body=str(res.status_code) + res.text, name="响应数据:",
                      attachment_type=allure.attachment_type.TEXT)
conftest.py - 会话之前清除数据
import pytest
from common.yaml_util import clear_extract_yaml

@pytest.fixture(scope="session",autouse=True)
def clear_extract():
    """ 每次会话之前清除 extract.yml 数据 """
    clear_extract_yaml()
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值