其他测试框架 自动化测试中使用自定义的方法 -- 简单实现

柒意 · 2021年02月27日 · 最后由 柒意 回复于 2021年03月23日 · 650 次阅读

0. 前言

apiAutoTest 从去年 8 月以来开源至今,也更新了不少内容,一起来看看吧

  • 第一个版本
- 2020/08/08 增加实际响应存储数据的方法,并在字典可以处理依赖见tools/svae_response.py

- 2020/08/09 实现多文件上传,接口中Path参数依赖处理
初步实现用迭代的方式来处理接口中的数据依赖关系
  • 第二个版本
- 2020/11/18 使用re库替换之前的字典迭代方式来处理数据依赖
- 2020/11/21 config.yaml文件中新增基准header设置
- 2020/11/22 支持预期结果中字段断言时使用语法糖来实现动态字段断言
- 2020/12/08 优化断言信息,增加数据库(支持mysql)查询操作, 使用`@pytest.fixture(scope="session")`来托管数据库对象,用例新增sql栏
- 2020/12/16 使用conftest.py 初始化用例, 增加失败重跑机制, 增加运行文件run,优化test_api.py冗余代码
  • 第三个版本
- 2021/01/19 添加数据清洗功能(测试开始前进行数据库备份-分别在服务器和本地进行,测试结束后将备份用以恢复数据-将尝试从服务器和本地恢复到服务器数据库中,docker部署的mysql服务已本地调试通过,直接linux部署的mysql并未测试)
- 2021/02/27 添加hooks.py文件(可在此处自定义方法,并用于用例当中,注意请务必在定义的方法中使用return),移除上次更新的eval语法糖,增加用例处理前的日志

一度说不会再更新维护代码,结果还是慢慢的更新了...

1. 自定义函数实现的故事

这是今天更新的,主要需求来自一个 apiAutoTest 的学习者反馈,这里感谢他,在此之前另一个小伙伴说他需要用上个接口返回的 id 字段进行运算, 很多测试框架都有这个功能,但我给 apiAutoTest 的定位是个工具,也就造个轮子嘛

2. 用例中如何使用自定义函数

2.1 在 tools/hooks.py 中定义好函数

def get_current_highest():
    """获取当前时间戳"""
    return int(time.time())


def sum_data(a, b):
    """计算函数"""
    return a + b

2.2 在用例中如何使用该函数

语法糖: @ 函数名 ()@: 使用无参数函数

@ 函数名 (参数 1, 参数 2)@: 向函数传递参数

ps: 函数参数兼容 apiAutoTest 中的提取依赖语法&此处为jsonpath语法&

用例 (怕截图不清所以就这里简易模拟了两条)

运行日志 (运行上述用例得到日志如下)


2021-02-27 16:06:50.538 | DEBUG    | api.base_requests:send_request:34 - 用例进行处理前数据: 
 接口路径: login 
 请求参数: {"username": "admin", "password": "123456"} 
 后置sql: select * from user where id=&$.case_002.data.id&; 
 预期结果: {"$.meta":{ "msg": "登录成功", "status": 200 }}
2021-02-27 16:06:50.765 | INFO     | api.base_requests:send_api:81 - 
最终请求地址:http://www.ysqorz.top:8888/api/private/v1/login
请求方法:post
请求头:{'Accept-Encoding': 'gzip, deflate', 'Accept-Language': 'zh-CN,zh;q=0.9', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36'}
请求参数:{'username': 'admin', 'password': '123456'}
上传文件:None
响应数据:{'data': {'id': 500, 'rid': 0, 'username': 'admin', 'mobile': '12345678', 'email': 'adsfad@qq.com', 'token': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjUwMCwicmlkIjowLCJpYXQiOjE2MTQ0MTMyMTAsImV4cCI6MTYxNDQ5OTYxMH0.cZTYLARKNj8SKlPGPdIUh9RmyQaYAJnJrLObaKiNiU4'}, 'meta': {'msg': '登录成功', 'status': 200}}
2021-02-27 16:06:50.773 | INFO     | tools.data_process:save_response:27 - 添加key: case_001, 对应value: {'data': {'id': 500, 'rid': 0, 'username': 'admin', 'mobile': '12345678', 'email': 'adsfad@qq.com', 'token': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjUwMCwicmlkIjowLCJpYXQiOjE2MTQ0MTMyMTAsImV4cCI6MTYxNDQ5OTYxMH0.cZTYLARKNj8SKlPGPdIUh9RmyQaYAJnJrLObaKiNiU4'}, 'meta': {'msg': '登录成功', 'status': 200}}
2021-02-27 16:06:50.775 | INFO     | tools.data_process:assert_result:115 - 第1个断言,实际结果:{'msg': '登录成功', 'status': 200} | 预期结果:{'msg': '登录成功', 'status': 200} 
断言结果 True
2021-02-27 16:06:50.775 | DEBUG    | api.base_requests:send_request:34 - 用例进行处理前数据: 
 接口路径: users/@sum_data(&$.case_001.data.id&, 2)@/ 
 请求参数: {"username": "tery","password": @sum_data(&$.case_001.data.id&, 66)@, "timer":  @get_current_highest()@, "timer_str": " @get_current_highest()@"} 
 后置sql:  
 预期结果: {"$.meta":{"msg": "设置状态成功", "status": 200}}
2021-02-27 16:06:50.775 | DEBUG    | tools:rep_expr:45 - &$.case_001.data.id& 替换的值为 500 
2021-02-27 16:06:50.775 | DEBUG    | tools:rep_expr:50 - 执行hooks函数sum_data(500, 2) 替换的值为 502
2021-02-27 16:06:50.775 | DEBUG    | tools:rep_expr:45 - &$.case_001.data.id& 替换的值为 500 
2021-02-27 16:06:50.783 | DEBUG    | tools:rep_expr:50 - 执行hooks函数sum_data(500, 66) 替换的值为 566
2021-02-27 16:06:50.783 | DEBUG    | tools:rep_expr:50 - 执行hooks函数get_current_highest() 替换的值为 1614413210
2021-02-27 16:06:50.783 | DEBUG    | tools:rep_expr:50 - 执行hooks函数get_current_highest() 替换的值为 1614413210
2021-02-27 16:06:50.835 | INFO     | api.base_requests:send_api:81 - 
最终请求地址:http://www.ysqorz.top:8888/api/private/v1/users/502/
请求方法:put
请求头:{'Accept-Encoding': 'gzip, deflate', 'Accept-Language': 'zh-CN,zh;q=0.9', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36', 'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjUwMCwicmlkIjowLCJpYXQiOjE2MTQ0MTMyMTAsImV4cCI6MTYxNDQ5OTYxMH0.cZTYLARKNj8SKlPGPdIUh9RmyQaYAJnJrLObaKiNiU4'}
请求参数:{'username': 'tery', 'password': 566, 'timer': 1614413210, 'timer_str': ' 1614413210'}
上传文件:None
响应数据:{'data': {'id': 502, 'username': 'linken', 'role_id': 34}, 'meta': {'msg': '更新成功', 'status': 200}}
2021-02-27 16:06:50.835 | INFO     | tools.data_process:save_response:27 - 添加key: case_002, 对应value: {'data': {'id': 502, 'username': 'linken', 'role_id': 34}, 'meta': {'msg': '更新成功', 'status': 200}}
2021-02-27 16:06:50.835 | INFO     | tools.data_process:assert_result:115 - 第1个断言,实际结果:{'msg': '更新成功', 'status': 200} | 预期结果:{'msg': '设置状态成功', 'status': 200} 
断言结果 False
2021-02-27 16:06:53.418 | SUCCESS  | __main__:run:43 - 报告已生成

分析日志

从上述日志可以看出语法糖@sum_data(&$.case_001.data.id&, 2)@运行之后的结果为502, 其处理的顺序则是先&$.case_001.data.id& 提取出来得到的值是 500, 然后调用函数 sum_data(500, 2),然后运行这个函数并把结果 502 与@sum_data(&$.case_001.data.id&, 2)@进行替换.

3. 自定义函数实现代码

因为这里用例读取出来的内容是字符串,我影响中反射应该能做到这一点,然后就去找了下资料,然后我又找到getattr()这个内置函数但是这个函数不能解决用例带参数的函数问题,然后我把目光移到了exec()内置函数,该函数可以执行字符串的 Python 代码,然而我又遇到了问题,该函数里面执行的 Python 代码变量在其他函数中不能顺利取出来用,最后我找到了资料 在函数内部使用 locals() 得到一个局部变量字典,通过字典取值的方式 把 exec 中的变量 取出来

tools/hooks.py

#!/usr/bin/env python
# _*_ coding: utf-8 _*_
"""
@project: apiAutoTest
@file: hooks.py
@author: zy7y
@time: 2021/2/27
@site: https://cnblogs.com/zy7y
@github: https://github.com/zy7y
@gitee: https://gitee.com/zy7y
@desc: 扩展方法, 2021/02/27
关于exec执行python代码可查阅资料:https://python3-cookbook.readthedocs.io/zh_CN/latest/c09/p23_executing_code_with_local_side_effects.html

"""
import time

def exec_func(func: str) -> str:
    """执行函数(exec可以执行Python代码)
    :params func 字符的形式调用函数
    : return 返回的将是个str类型的结果
    """
    # 得到一个局部的变量字典,来修正exec函数中的变量,在其他函数内部使用不到的问题
    loc = locals()
    exec(f"result = {func}")
    return str(loc['result'])


def get_current_highest():
    """获取当前时间戳"""
    return int(time.time())


def sum_data(a, b):
    """计算函数"""
    return a + b

tools/__init__.py

写到这里发现篇幅过长了,所以这里就把改动的地方代码贴出来吧

#!/usr/bin/env/python3
# -*- coding:utf-8 -*-
"""
@project: apiAutoTest
@author: zy7y
@file: __init__.py.py
@ide: PyCharm
@time: 2020/7/31
"""
import json
import re
import allure
from jsonpath import jsonpath
from loguru import logger

from tools.hooks import *


... 上方代码省略


def rep_expr(content: str, data: dict, expr: str = '&(.*?)&') -> str:
    """从请求参数的字符串中,使用正则的方法找出合适的字符串内容并进行替换
    :param content: 原始的字符串内容
    :param data: 在该项目中一般为响应字典,从字典取值出来
    :param expr: 查找用的正则表达式
    return content: 替换表达式后的字符串
    """
    for ctt in re.findall(expr, content):
        content = content.replace(f'&{ctt}&', str(extractor(data, ctt)))
        logger.debug(f"&{ctt}& 替换的值为 {str(extractor(data, ctt))} ")

    # 增加自定义函数得的调用,函数写在tools/hooks.py中
    for func in re.findall('@(.*?)@', content):
        content = content.replace(f'@{func}@', exec_func(func))
        logger.debug(f"执行hooks函数{func} 替换的值为 {exec_func(func)}")

    return content

... 下方代码省略

4. 源码地址

github: https://www.github.com/zy7y/apiAutoTest

gitee: https://www.gitee.com/zy7y/apiAutoTest

其中最早版本 (采用字典迭代方式处理依赖) 在 version1.0 分支

5. 致谢

感谢所有给予我帮助的人,文章,正在学习或使用 apiAutoTest 的同学们,其实个人开源这个项目以来个人有得到成就感,非常感谢

参考资料:

python3-cookbook

6. 往日文章

apiAutoTest: 开源啦

apiAutoTest: 使用 re 库来处理接口之前的数据依赖

apiAutoTest: 增加数据隔离 (测试前后备份/还原数据库)

apiAutoTest: 增加自定义函数,用例中可以调用

共收到 6 条回复 时间 点赞

实现路上都是查的资料,用的简单的函数,第三方库个人能力停留在库的使用,不明白其原理,打扰了

2楼 已删除
柒意 关闭了讨论 03月14日 12:39
柒意 重新开启了讨论 03月22日 07:15

书上说还有装饰器的方法来实现,不知道各位有什么好点子没有...

感觉有点复杂了。这种自定义语法除非很好记,否则会严重影响可读性且容易留坑。比如上面这个格式,没有考虑转义这个场景,如果用户刚好要定义的是 api?a=1&b=2&c=4 ,这里的 &b=2& 是不是有可能会被正则匹配到然后替换?

如果确实要引入函数语法,个人建议参考 Jmeter 或者 RobotFramework 的语法来,相对好记和容易识别。

像这种场景我是在项目里面挂载 python 工程,通过映射关系动态加载自定义关键字跟方法

小酷 回复

👌,后面补补相关知识,谢谢

柒意 #10 · 2021年03月23日 Author
陈恒捷 回复

好的,谢谢

柒意 关闭了讨论 03月23日 02:07
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册