Python接口自动化测试框架: pytest+allure+jsonpath+requests+excel实现的接口自动化测试框架(学习成果)

废话

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

问题

整体代码结构优化未实现,导致最终测试时间变长,其他工具单接口测试只需要39ms,该框架中使用了101ms,考虑和频繁读写用例数据导致

环境与依赖
名称 版本 作用
python 3.7.8
pytest 6.0.1 底层单元测试框架,用来实现参数化,自动执行用例
allure-pytest 2.8.17 allure与pytest的插件可以生成allure的测试报告
jsonpath 0.82 用来进行响应断言操作
loguru 0.54 记录日志
PyYAML 5.3.1 读取yml/yaml格式的配置文件
Allure 2.13.5 要生成allure测试报告必须要在本机安装allure并配置环境变量
xlrd 1.2.0 用来读取excel中用例数据
xlutils 2.0.0 用来向excel中写入实际的响应结果
yagmail 0.11.224 测试完成后发送邮件
requests 2.24.0 发送请求
目录结构

执行顺序

运行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回写实际响应到用例文件中(方便根据依赖提取对应的数据)

config.ymal展示
server:

服务器host地址,发送请求的url= host+ path

test: http://127.0.0.1:8888/api/private/v1/
dev: http://47.115.124.102:8888/api/private/v1/ response_reg:
# 提取token的表达式
token: \(.data.token<br/> # 提取实际响应中的某部分来作为断言数据(实例中断言的是meta这个子字典,预期结果也是写的meta子字典中的内容)<br/> response: \).meta file_path:

# 测试用例数据地址<br/>

case_data: ../data/case_data.xlsx
# 运行测试存储的结果路径
report_data: ../report/data/
# 本地测试报告生成位置
report_generate: ../report/html/
# 压缩本地测试报告后的路径
report_zip: ../report/html/apiAutoTestReport.zip
# 日志文件地址
log_path: ../log/运行日志{time}.log email:
user: 发件人邮箱
password: 邮箱授权码(不是密码)
host: smtp.163.com
contents: 解压apiAutoReport.zip(接口测试报告)后,请使用已安装Live Server 插件的VsCode,打开解压目录下的index.html查看报告
# 发件人列表
addressees: [“123@qq.com”,“12067@qq.com”,“717@qq.com”]
title: 接口自动化测试报告(见附件)
# 测试报告附件
enclosures: [“../report/html/apiAutoTestReport.zip”,]

EXcel用例展示

脚本一览
#!/usr/bin/env/python3

-- coding:utf-8 --

“”“
@project: apiAutoTest
@author: zy7y
@file: base_requests.py
@ide: PyCharm
@time: 2020/7/31
”“”
from loguru import logger
import requests class BaseRequest(object):

def __init__(self):<br/>
    pass

请求

def base_requests(self, method, url, data=None, file_var=None, file_path=None, header=None):<br/>
    &#34;&#34;&#34;

:param method: 请求方法

    :param url: 接口path<br/>
    :param data: 数据,请传入dict样式的字符串<br/>
    :param file_path: 上传的文件路径<br/>
    :param file_var: 接口中接收文件对象的参数名<br/>
    :param header: 请求头<br/>
    :return: 完整的响应对象<br/>
    &#34;&#34;&#34;<br/>
    session = requests.Session()<br/>
    if (file_var in [None, &#39;&#39;]) and (file_path in [None, &#39;&#39;]):<br/>
        files = None<br/>
    else:<br/>
        # 文件不为空的操作<br/>
        files = {file_var: open(file_path, &#39;rb&#39;)}<br/>
    # get 请求参数传递形式 params<br/>
    if method == &#39;get&#39;:<br/>
        res = session.request(method=method, url=url, params=data, headers=header)<br/>
    else:<br/>
        res = session.request(method=method, url=url, data=data, files=files, headers=header)<br/>
    logger.info(f&#39;请求方法:{method},请求路径:{url}, 请求参数:{data}, 请求文件:{files}, 请求头:{header})&#39;)<br/>
    return res.json()

#!/usr/bin/env/python3

-- coding:utf-8 --

“”“
@project: apiAutoTest
@author: zy7y
@file: read_data.py
@ide: PyCharm
@time: 2020/7/31
”“”
import json import jsonpath
import xlrd
from xlutils.copy import copy
from loguru import logger class ReadData(object):

def __init__(self, excel_path):<br/>
    self.excel_file = excel_path<br/>
    self.book = xlrd.open_workbook(self.excel_file)

def get_data(self):

    &#34;&#34;&#34;

:return:

    &#34;&#34;&#34;<br/>
    data_list = []<br/>
    title_list = []

table = self.book.sheet_by_index(0)

    for norw in range(1, table.nrows):<br/>
        # 每行第4列 是否运行<br/>
        if table.cell_value(norw, 3) == &#39;否&#39;:<br/>
            continue<br/>
        # 每行第3列, 标题单独拿出来<br/>
        title_list.append(table.cell_value(norw, 1))

返回该行的所有单元格组成的数据 table.row_values(0) 0代表第1列

        case_number = table.cell_value(norw, 0)<br/>
        path = table.cell_value(norw, 2)<br/>
        is_token = table.cell_value(norw, 4)<br/>
        method = table.cell_value(norw, 5)<br/>
        file_var = table.cell_value(norw, 6)<br/>
        file_path = table.cell_value(norw, 7)<br/>
        dependent = table.cell_value(norw, 8)<br/>
        data = table.cell_value(norw, 9)<br/>
        expect = table.cell_value(norw, 10)<br/>
        actual = table.cell_value(norw, 11)<br/>
        value = [case_number, path, is_token, method, file_var, file_path, dependent, data, expect, actual]<br/>
        logger.info(value)<br/>
        # 配合将每一行转换成元组存储,迎合 pytest的参数化操作,如不需要可以注释掉 value = tuple(value)<br/>
        value = tuple(value)<br/>
        data_list.append(value)<br/>
    return data_list, title_list

def write_result(self, case_number, result):

    &#34;&#34;&#34;

:param case_number: 用例编号:case_001

    :param result: 需要写入的响应值<br/>
    :return:<br/>
    &#34;&#34;&#34;<br/>
    row = int(case_number.split(&#39;_&#39;)[1])<br/>
    logger.info(&#39;开始回写实际响应结果到用例数据中.&#39;)<br/>
    result = json.dumps(result, ensure_ascii=False)<br/>
    new_excel = copy(self.book)<br/>
    ws = new_excel.get_sheet(0)<br/>
    # 11 是 实际响应结果栏在excel中的列数-1<br/>
    ws.write(row, 11, result)<br/>
    new_excel.save(self.excel_file)<br/>
    logger.info(f&#39;写入完毕:-写入文件: {self.excel_file}, 行号: {row + 1}, 列号: 11, 写入值: {result}&#39;)

读实际的响应

def read_actual(self, depend):<br/>
    &#34;&#34;&#34;

:param nrow: 列号

    :param depend: 依赖数据字典格式,前面用例编号,后面需要提取对应字段的jsonpath表达式<br/>
    {&#34;case_001&#34;:[&#34;$.data.id&#34;,],}<br/>
    :return:<br/>
    &#34;&#34;&#34;<br/>
    depend = json.loads(depend)<br/>
    # 用来存依赖数据的字典<br/>
    depend_dict = {}<br/>
    for k, v in depend.items():<br/>
        # 得到行号<br/>
        norw = int(k.split(&#39;_&#39;)[1])<br/>
        table = self.book.sheet_by_index(0)<br/>
        # 得到对应行的响应,        # 11 是 实际响应结果栏在excel中的列数-1<br/>
        actual = json.loads(table.cell_value(norw, 11))<br/>
        try:<br/>
            for i in v:<br/>
                logger.info(f&#39;i {i}, v {v}, actual {actual} \n {type(actual)}&#39;)<br/>
                depend_dict[i.split(&#39;.&#39;)[-1]] = jsonpath.jsonpath(actual, i)[0]<br/>
        except TypeError as e:<br/>
            logger.error(f&#39;实际响应结果中无法正常使用该表达式提取到任何内容,发现异常{e}&#39;)<br/>
    return depend_dict<br/>

#!/usr/bin/env/python3

-- coding:utf-8 --

“”“
@project: apiAutoTest
@author: zy7y
@file: test_api.py
@ide: PyCharm
@time: 2020/7/31
”“”
import json
import shutil import jsonpath
from loguru import logger
import pytest
import allure
from api.base_requests import BaseRequest
from tools.read_config import ReadConfig
from tools.read_data import ReadData rc = ReadConfig()
base_url = rc.read_serve_config(‘dev’)
token_reg, res_reg = rc.read_response_reg()
case_data_path = rc.read_file_path(‘case_data’)
report_data = rc.read_file_path(‘report_data’)
report_generate = rc.read_file_path(‘report_generate’)
log_path = rc.read_file_path(‘log_path’)
report_zip = rc.read_file_path(‘report_zip’)
email_setting = rc.read_email_setting() data_list, title_ids = ReadData(case_data_path).get_data() br = BaseRequest()
token_header = {}
no_token_header = {} class TestApiAuto(object): def start_run_test(self):

    import os<br/>
    if os.path.exists(&#39;../report&#39;) and os.path.exists(&#39;../log&#39;):<br/>
        shutil.rmtree(path=&#39;../report&#39;)<br/>
        shutil.rmtree(path=&#39;../log&#39;)<br/>
    logger.add(log_path)

pytest.main(args=[f‘–alluredir={report_data}’])

    # # 启动一个web服务的报告<br/>
    # os.system(&#39;allure serve ./report/data&#39;)<br/>
    os.system(f&#39;allure generate {report_data} -o {report_generate} --clean&#39;)<br/>
    logger.debug(&#39;报告已生成&#39;)

def treating_data(self, is_token, dependent, data):

    if is_token == &#39;&#39;:<br/>
        header = no_token_header<br/>
    else:<br/>
        header = token_header<br/>
    logger.info(f&#39;处理依赖时data的数据:{data}&#39;)<br/>
    if dependent != &#39;&#39;:<br/>
        dependent_data = ReadData(case_data_path).read_actual(dependent)<br/>
        logger.debug(f&#39;依赖数据解析获得的字典{dependent_data}&#39;)<br/>
        if data != &#39;&#39;:<br/>
            # 合并组成一个新的data<br/>
            dependent_data.update(json.loads(data))<br/>
            data = dependent_data<br/>
            logger.debug(f&#39;data有数据,依赖有数据时 {data}&#39;)<br/>
        else:<br/>
            # 赋值给data<br/>
            data = dependent_data<br/>
            logger.debug(f&#39;data无数据,依赖有数据时 {data}&#39;)<br/>
    else:<br/>
        if data == &#39;&#39;:<br/>
            data = None<br/>
            logger.debug(f&#39;data无数据,依赖无数据时 {data}&#39;)<br/>
        else:<br/>
            data = json.loads(data)<br/>
            logger.debug(f&#39;data有数据,依赖无数据 {data}&#39;)<br/>
    return data, header

@pytest.mark.parametrize(‘case_number,path,is_token,method,file_var,’

                         &#39;file_path,dependent,data,expect,actual&#39;, data_list, ids=title_ids)<br/>
def test_main(self, case_number, path, is_token, method, file_var, file_path,<br/>
              dependent, data, expect, actual):

with allure.step(“处理相关数据依赖,header”):

        data, header = self.treating_data(is_token, dependent, data)<br/>
    with allure.step(&#34;发送请求,取得响应结果的json串&#34;):<br/>
        res = br.base_requests(method=method, url=base_url + path, file_var=file_var, file_path=file_path,<br/>
                               data=data, header=header)<br/>
    with allure.step(&#34;将响应结果的内容写入用例中的实际结果栏&#34;):<br/>
        ReadData(case_data_path).write_result(case_number, res)

写token的接口必须是要正确无误能返回token的

        if is_token == &#39;写&#39;:<br/>
            with allure.step(&#34;从登录后的响应中提取token到header中&#34;):<br/>
                token_header[&#39;Authorization&#39;] = jsonpath.jsonpath(res, token_reg)[0]<br/>
        logger.info(f&#39;token_header: {token_header}, \n no_token_header: {no_token_header}&#39;)<br/>
    with allure.step(&#34;根据配置文件的提取响应规则提取实际数据&#34;):<br/>
        really = jsonpath.jsonpath(res, res_reg)[0]<br/>
    with allure.step(&#34;处理读取出来的预期结果响应&#34;):<br/>
        expect = eval(expect)<br/>
    with allure.step(&#34;预期结果与实际响应进行断言操作&#34;):<br/>
        assert really == expect<br/>
        logger.info(f&#39;完整的json响应: {res}\n 需要校验的数据字典: {really}\n 预期校验的数据字典: {expect}\n 测试结果: {really == expect}&#39;)

if name == ‘main’:

from tools.zip_file import zipDir<br/>
from tools.send_email import send_email<br/>
t1 = TestApiAuto()<br/>
t1.start_run_test()<br/>
zipDir(report_generate, report_zip)<br/>
send_email(email_setting)

运行结果

致谢

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

源码地址