新手区 pytest+requests+allure-pytest+jsonpath+xlrd 接口自动化测试 (学习成果)

· 2020年08月05日 · 最后由 回复于 2020年11月21日 · 8188 次阅读

废话 (初次发文日期 2020 年 8 月)

最近在自己学习接口自动化测试,这里也算是完成一个小的成果,欢迎大家交流指出不合适的地方,源码在文末

功能 (2022 更新)

  • 测试数据隔离: 测试前后进行数据库备份/还原
  • 接口间数据依赖: 需要 B 接口使用 A 接口响应中的某个字段作为参数
  • 自定义扩展方法: 在用例中使用自定义方法 (如:获取当前时间戳...) 的返回值
  • 接口录制:录制指定包含 url 的接口,生成用例数据
  • 用例跳过:支持表达式、内置函数、调用变量实现条件跳过用例
  • 动态多断言: 可(多个)动态提取实际预期结果与指定的预期结果进行比较断言操作
  • 对接数据库: 讲数据库的查询结果可直接用于断言操作
  • 邮件发送:将 allure 报告压缩后已附件形式发送

运行机制

  1. 通过读取配置文件,获取到 host 地址、提取 token 的 jsonpath 表达式,提取实际响应结果用来与预期结果比对的 jsonpath 表达式。
  2. 读取 excel 用例文件数据,组成一个符合 pytest 参数化的用例数据,根据每列进行数据处理(token 操作、数据依赖)
  3. token,写,需要使用一个正常登录的接口,并且接口中要返回 token 数据,才可以提取,token,读为该请求将携带有 token 的 header,token 无数据的将不携带 token
  4. 数据依赖处理,从 excel 中读取出来的格式{"用例编号":["jsonpath 表达式 1", "jsonpath 表达式 2"]},通过用例编号来获取对应 case 的实际响应结果(实际响应结果在发送请求后,回写到 excel 中),通过 jsonpath 表达式提取对应的依赖参数字段,以及对应的值,最终会返回一个存储该接口需要依赖数据的字典如{"userid":500, "username": "zy7y"},在发送请求时与请求数据进行合并,组成一个新的 data 放到请求中
  5. 每次请求完成之后将回写实际的响应结果到 excel 中
  6. 根据配置文件中配置的 jsonpath 表达式提取实际响应内容与 excel 中预期结果的数据对比
  7. 生成测试报告
  8. 压缩测试报告文件夹
  9. 发送邮件

目录结构

35F236C2-2F64-4891-8384-2FBFE3229F90.png

执行顺序

运行 test_api.py -> 读取 config.yaml(tools.read_config.py) -> 读取 excel 用例文件 (tools.read_data.py) -> test_api.py 实现参数化 -> 处理是否依赖数据 ->base_requests.py 发送请求 -> test_api.py 断言 -> read_data.py 回写实际响应到用例文件中 (方便根据依赖提取对应的数据)

EXcel 用例展示

Snipaste_2020-08-13_11-21-18.png
用例格式说明

运行结果

Snipaste_2020-08-03_15-54-45.png

致谢

jsonpath 语法学习:https://blog.csdn.net/liuchunming033/article/details/106272542

zip 文件压缩:https://www.cnblogs.com/yhleng/p/9407946.html

这算是学习接口自动化的第一个成果,但是要应用生产环境,拿过去还需要改很多东西,欢迎交流。

视频记录 (该视频指向tag2.0)

前往获取源码

附言 1  ·  2020年12月08日

更新:2020/12/08 支持 mysql 数据库 的查询(单条数据)并可以 用响应结果与查询结果来断言,打扰了

附言 2  ·  2020年11月22日

更新 2020/11/23

最新用例截图以及用例填写格式

case.png

Snipaste_2020-11-23_07-21-53.png

数据依赖/路径参数依赖

我理解的参数依赖/接口依赖就是接口进行关联操作,比如有些查询接口需要登录之后才可以操作,那么我们就需要拿到 token 之类的东西,这一部分东西是放到 header 中的,apiAutoTest 围绕的只有路径参数依赖,请求数据依赖

  • 路径参数依赖

譬如说现在的 restful,一个 users 接口,路由一般这样的users他的请求方式是 get,这个路由我们把他认为是查所有用户,如果查某一个用户可能是这样的users/:id也是个 get 请求,这里这个 id 想表达的意思是这里有个需要个用户 id 的参数,比如 1-500 里面的任意 1 个,也就是说这个 id 是可变的,可以从登录接口的返回响应取一个叫 userId 的值

  • 请求参数依赖

这个应该好理解些,就是说支付接口需要的订单 id,是从上一步提交订单接口返回的响应订单 id

举个例子

假设现在有个实际响应结果字典如下

{"case_002": {
        "data": {
            "id": 500,
            "username": "admin",
            "mobile": "12345678",
        }},
  "case_005": {
        "data": {
            "id": 511,
            "create_time": 1605711095
        },
    }
}
  • excel 中接口路径内容:users/&$.case_005.data.id&/state/&$.case_005.data.careate_time&

代码内部解析后如下:users/511/state/1605711095

&$.case_005.data.id& 代表从响应字典中提取 case_005 字典中 data 字典中的 id 的值,提取出来的结果是 511

  • excel 中请求参数内容如下:
{
 "pagenum": 1, 
 "pagesize": "12",
 "data": &$.case_005.data&, 
 "userId": &$.case_002.data.id&
}

代码内部解析后如下:

{
 "pagenum": 1, 
 "pagesize": "12",
 "data": {
            "id": 511,
            "create_time": 1605711095
        }, 
 "userId": 500
}

其实不难看出其中规则&jsonpath提取语法&,如果你需要的内容是字符串类型,只需要这样"&jsonpath提取语法&"

上传文件

用例中书写格式,在上传文件栏

# 单文件上传在excel中写法
{"接口中接受文件对象的参数名": "文件路径地址"}

# 多文件上传在excel中写法
{"接口中接受文件对象的参数名": ["文件路径1", "文件路径2"]}

预期结果

用例书写格式

# 断言一个内容
{"jsonpath提取表达式": 预期结果内容}
# 多个断言
{"jsonpath提取表达式1": 预期结果内容1,"jsonpath提取表达式2": 预期结果内容2}

其他优化

  • config.yaml 文件中新增可配置初始 header,整体代码优化,相比之前,同样测试用例执行下,快了 2s 左右
  • 将配置文件读取,用例读取整合在read_file.py
  • 移除报告压缩方法
  • 减少日志信息

现依赖处理代码

tools/init.py

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

import allure

from jsonpath import jsonpath
from loguru import logger


def extractor(obj: dict, expr: str = '.') -> object:
    """
    根据表达式提取字典中的value,表达式, . 提取字典所有内容, $.case 提取一级字典case, $.case.data 提取case字典下的data
    :param obj :json/dict类型数据
    :param expr: 表达式, . 提取字典所有内容, $.case 提取一级字典case, $.case.data 提取case字典下的data
    $.0.1 提取字典中的第一个列表中的第二个的值
    """
    try:
        result = jsonpath(obj, expr)[0]
    except Exception as e:
        logger.error(f'提取不到内容,丢给你一个错误!{e}')
        result = None
    return result


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)))
    return content


def convert_json(dict_str: str) -> dict:
    """
    :param dict_str: 长得像字典的字符串
    return json格式的内容
    """
    try:
        if 'None' in dict_str:
            dict_str = dict_str.replace('None', 'null')
        elif 'True' in dict_str:
            dict_str = dict_str.replace('True', 'true')
        elif 'False' in dict_str:
            dict_str = dict_str.replace('False', 'false')
        dict_str = json.loads(dict_str)
    except Exception as e:
        if 'null' in dict_str:
            dict_str = dict_str.replace('null', 'None')
        elif 'true' in dict_str:
            dict_str = dict_str.replace('true', 'True')
        elif 'False' in dict_str:
            dict_str = dict_str.replace('false', 'False')
        dict_str = eval(dict_str)
        logger.error(e)
    return dict_str


def allure_title(title: str) -> None:
    """allure中显示的用例标题"""
    allure.dynamic.title(title)


def allure_step(step: str, var: str) -> None:
    """
    :param step: 步骤及附件名称
    :param var: 附件内容
    """
    with allure.step(step):
        allure.attach(json.dumps(var, ensure_ascii=False, indent=4), step, allure.attachment_type.TEXT)

tools/data_process.py

#!/usr/bin/env/python3
# -*- coding:utf-8 -*-
"""
@project: apiAutoTest
@author: zy7y
@file: data_process.py
@ide: PyCharm
@time: 2020/11/18
"""
from tools import logger, extractor, convert_json, rep_expr, allure_step
from tools.read_file import ReadFile


class DataProcess:
    response_dict = {}
    header = ReadFile.read_config('$.request_headers')
    have_token = header.copy()

    @classmethod
    def save_response(cls, key: str, value: object) -> None:
        """
        保存实际响应
        :param key: 保存字典中的key,一般使用用例编号
        :param value: 保存字典中的value,使用json响应
        """
        cls.response_dict[key] = value
        logger.info(f'添加key: {key}, 对应value: {value}')

    @classmethod
    def handle_path(cls, path_str: str) -> str:
        """路径参数处理
        :param path_str: 带提取表达式的字符串 /&$.case_005.data.id&/state/&$.case_005.data.create_time&
        上述内容表示,从响应字典中提取到case_005字典里data字典里id的值,假设是500,后面&$.case_005.data.create_time& 类似,最终提取结果
        return  /511/state/1605711095
        """
        # /&$.case.data.id&/state/&$.case_005.data.create_time&
        return rep_expr(path_str, cls.response_dict)

    @classmethod
    def handle_header(cls, token: str) -> dict:
        """处理header
        :param token: 写: 写入token到header中, 读: 使用带token的header, 空:使用不带token的header
        return
        """
        if token == '读':
            return cls.have_token
        else:
            return cls.header

    @classmethod
    def handler_files(cls, file_obj: str) -> object:
        """file对象处理方法
        :param file_obj: 上传文件使用,格式:接口中文件参数的名称:"文件路径地址"/["文件地址1", "文件地址2"]
        实例- 单个文件: &file&D:
        """
        if file_obj == '':
            return
        for k, v in convert_json(file_obj).items():
            # 多文件上传
            if isinstance(v, list):
                files = []
                for path in v:
                    files.append((k, (open(path, 'rb'))))
            else:
                # 单文件上传
                files = {k: open(v, 'rb')}
        return files

    @classmethod
    def handle_data(cls, variable: str) -> dict:
        """请求数据处理
        :param variable: 请求数据,传入的是可转换字典/json的字符串,其中可以包含变量表达式
        return 处理之后的json/dict类型的字典数据
        """
        if variable == '':
            return
        data = rep_expr(variable, cls.response_dict)
        variable = convert_json(data)
        return variable

    @classmethod
    def assert_result(cls, response: dict, expect_str: str):
        """ 预期结果实际结果断言方法
        :param response: 实际响应字典
        :param expect_str: 预期响应内容,从excel中读取
        return None
        """
        expect_dict = convert_json(expect_str)
        index = 0
        for k, v in expect_dict.items():
            actual = extractor(response, k)
            index += 1
            logger.info(f'第{index}个断言,实际结果:{actual} | 预期结果:{v} \n断言结果 {actual == v}')
            allure_step(f'第{index}个断言',  f'实际结果:{actual} = 预期结果:{v}')
            assert actual == v

源码地址

master: 分支为最新代码

version1.0: 分支为之前开源的代码(通过字典迭代的方式来处理数据依赖)

Https://gitee.com/zy7y/apiAutoTest.git

Https://github.com/zy7y/apiAutoTest.git

后续打算

目前在公司正在做接口测试,说实话也是摸索着来,以上的优化项都是实际做的过程中突然想到的,然后就更新了

  • 接入用例前后置 SQL, 前置 SQL 目前想的是现在项目中遇到的问题,有些接口没有返回需要的数据,这里就要用前置 SQL 查询的结果传到请求数据里面了,后置 SQL 主要是请求后查看数据库中的数据是否变动,形成数据库断言
  • 企业微信推送:目前项目中预想的效果,是后端人员提交代码,自动部署之后,通过 gitlab-ci 启动测试代码,进行接口测试完成之后采集 allure 中的测试结果一有异常/失败用例就发送邮件并进行企业微信推送给领导
  • .... 就不说了还有很多优化项,能力不够好好充电吧,~~

致谢

谢谢各位对 apiAutoTest 的帮助,谢谢~,打扰了

附言 3  ·  2020年09月10日

关联下这篇帖子:希望大家能给我些建议--https://testerhome.com/topics/25418

附言 4  ·  2020年08月12日

https://gitee.com/zy7y/apiAutoTestDemo 完结,gitee 库中已添加对应使用项目的接口文档,用例书写格式描述文档使用了 pytest.mark.parametrize() 实现参数化,字典存储实际结果响应读取 data 依赖,path 参数依赖,重组 data 以及 path,增加了多文件上传,按道理是支持 restful 接口规范的,其中处理请求数据依赖,path 数据依赖的方法可能过于麻烦 (能力有限目前只能想到这种处理方法),如果在学习的朋友没有:学习 上面的某种思路,希望这个东西能帮到你们:,如果有更好的处理方法希望大家能交流下。(该代码有很多异常情况并没有进行处理,所以需要严格按照用例格式进行用例书写),若之前有小伙伴使用了这个 Demo 的 请将基准地址更换为(我私有的云服务器,已避免不必要的麻烦):http://49.232.203.244:8888/api/private/v1/

apiAutoTest用例书写格式

关于之前的日志一个问题,在 allure 报告中展示比较乱,可使用日志旁边放大按钮 (会在当前页面弹出一个页面显示日志,会得到部分改善),如有需要交流,可联系我:QQ 396667207,加好友请注明来意,谢谢给我留帖的各位。

共收到 50 条回复 时间 点赞

学了两年 Python 还只会用 if else 的 点点点测试 (目前已失业), 欢迎大家交流,指出不足!

不错,值得学习。Excel 维护测试用例感觉有点费力,不方便,还有断言是怎么封装的?我一直在纠结这个断言不知如何封装,我是用 JSON 和 YAML 来维护测试用例的。团队合作时还是要有个平台化的界面会更适合。

我目前做的和你很类似不过我的邮件这块处理是通过 jenkins 的插件来实现发送 不过楼主这个写的又很多值得我细细推敲和学习的地方

我最近学了 django+vue+element 我一直在想这个怎么和自动化测试脚本结合起来

赞 数据驱动的测试用例在造数据的时候比较头疼,excel 看起来比较直观但是执行中 IO 操作太多了,而且操作每行每列花费很多时间,而且万一以后数据格式发生变化,看起来用例里的生成数据的方法都得改变
我在实践中会将用到得数据写成.json 文件保存,直接读取;另一个办法是写一个模板,再更新数据的时候根据模板生成,规定好类型

对了,还有日志系统,日志显示的太乱了,加上分割符号以及步骤说明,不然排错的时候就哭了

#7 · 2020年08月05日 Author
wuwei 回复

在配置文件里面配置了个用来断言的实际响应 jsonpath 表达式 ,通过它来实现提取实际响应某个子字典的内容实例中是提取了 meta 字典{"msg":"msg 内容", "status": 200}, excel 中 也使用的 meta 子字典部分进行断言

#8 · 2020年08月05日 Author
GoodLuck 回复

对的这个里面 执行一个接口要 100ms, 用其他接口工具只要了 39ms,文中 回写实际响应结果,可以考虑写到一个字典而不是 excel 中了,使用用例编号作为 key,实际响应作为 value,来替换本来的请求后将实际结果回写到 excel 中

#9 · 2020年08月05日 Author
GoodLuck 回复

嗯,需要再去学习下日志的功能作用,其实不太理解日志等级这些,不过当务之急还是找工作就业...

#10 · 2020年08月05日 Author

集成 jenkins 了 不用单独在脚本里面定义邮件功能吧,我还没接触到 jenkins。。。

excel 维护用例也可以很方便

#12 · 2020年08月05日 Author
tianfuzhiguo 回复

挺直观的😀

大神,刚好是我想要的,先 mark 下,后面有空学学

#14 · 2020年08月05日 Author

并不是大神...,谢谢,一起学习

挺好的,支持分享

感谢分享

回复

加油

真正用这些的公司很少很少

#19 · 2020年08月05日 Author

应该就是把它封装成方法,平台中点一个按钮 这边开始运行吧,,,

#20 · 2020年08月05日 Author
大海 回复

谢谢,一起加油

#21 · 2020年08月05日 Author
西米呆蒂 回复

希望有所帮助,一起进步

仅楼主可见
#23 · 2020年08月05日 Author
天邪泪 回复

#24 · 2020年08月05日 Author
朱zhu 回复

嗯嗯,没进过大公司,重庆 4-6k,面试也是会问这方面的东西,公司可以不用,但是你得会

回复

是在 jenkins 里面用 shell 找到代码的地址然后去执行

回复

重庆工资感人啊 在上海这工资应届生都没这么低

回复

大佬 你就是看的那个哔哩哔哩的视频教程吗?你本来就会 Python 吗?感谢

#28 · 2020年08月06日 Author

python 自己学的,学了两年还只会 if else,看了点吧

回复

看了点 就这么厉害了 麻烦问一下大佬 你现在做的这些主要是看的什么教程啊 求分享 谢谢

楼主重庆的吗

#31 · 2020年08月07日 Author
阿杰 回复

四川广安人,在重庆找工作..

#32 · 2020年08月07日 Author

之前培训过 java 方向的测试开发,也就教了下 selenium、接口自动化 (读取 excel 执行用例),b 站上有很多机构对应的课程你可以去看看,这里不说详细了 免得打广告嫌疑

#33 · 2020年08月07日 Author

使用 jenkins 进行集成的朋友,代码中的邮件发送,报告生成,报告压缩方法可以干掉了,附上自己这两天的一个文章 (本人第一次使用 jenkins,有些欠佳的地方,还请大家指出)https://www.cnblogs.com/zy7y/p/13448102.html

重庆测试人报道,在光电园这边,也是年初才培训完,入职,我们组前几天还在招自动化的高手哦

#35 · 2020年08月07日 Author
大帅 回复

可能还是同一个机构哦,7 月 22 号才从麒麟 D 座出来

回复

我就在那里😂来面试的?如何

#37 · 2020年08月07日 Author
大帅 回复

之前在 9 楼,反正就是点点点

#38 · 2020年08月07日 Author

通过 jenkins,流水线任务构建,运行该代码出现:test/test_api.py:23: in
token_reg, res_reg = rc.read_response_reg()
E TypeError: 'NoneType' object is not iterable

暂无解决办法,需要的朋友可查看楼上的自由风格任务构建,另外如果有大佬有解决方法,还请指点下,谢谢

回复

我在 16 楼,是的,他这边工具,平台什么的比较完备,如果不能入内部,也很难学到什么了,只能靠自学,我就是指望着明年的一次机会了,不行的话也就只有换地方了

#40 · 2020年08月07日 Author
大帅 回复

中移?可能我们之前电梯还碰过面,哈哈

回复

是的,是这样的,难啊还是,还是祝前辈能找到满意的工作吧

#42 · 2020年08月07日 Author
大帅 回复

谢谢,都加油

仅楼主可见
#44 · 2020年08月11日 Author

加 QQ 吧,我 QQ:396667207

有个疑问,请求 body 的字段要从之前的某个接口的请求 body 取呢,怎么做?

#46 · 2020年08月17日 Author
阿里阿多 回复

目前实现的是根据依赖数据中的表达式提取出一依赖数据字典,然后直接与请求 body 中的数据合并、或者给依赖数据一个指定的 key,那么请求中的 body 就会将这个 key:{依赖数据字典} 直接合并到请求 body 中,如果请求 body 中有字段与依赖数据字典中的 key 相等,那么将在对应请求 body 下面的 key,所对应的 value 进行追加值(暂时支持本来 body 中该 key 对应的 value 是 list 类型或者字典类型)

仅楼主可见

谢谢楼主提供。好好学习

49楼 已删除
回复

可以了,我再写了个方法保存每次请求之前实际的请求 body;你那个保存响应的脚本非常有帮助,谢谢

[该话题已被删除] 中提及了此贴 11月19日 01:36
#52 · 2020年11月21日 Author

帖子中的版本在:version1.0 分支,最新可用版本在 master 分支上

关闭了讨论 03月03日 19:44
重新开启了讨论 06月15日 09:57
关闭了讨论 06月15日 09:58
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册