Python接口自动化测试框架: pytest+allure+jsonpath+requests+excel实现的接口自动化测试框架(已开源)

邮差的信 提交于 2020-08-10 08:49:08

废话

和几个朋友聊天然后出来的产物希望能帮到大家学习接口自动化测试,欢迎大家交流指出不合适的地方,源码在文末

功能

  1. 实现:get/post请求(上传文件)::理论上其他delete/put等请求也实现了
  2. 发送邮件
  3. 生成allure测试报告
  4. 压缩测试报告文件
  5. 数据依赖

运行机制

  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. 发送邮件

已知问题

执行接口消耗时间变长,代码乱(语言学的不扎实),频繁读写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 发送请求

目录结构

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

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
  # 提取实际响应中的某部分来作为断言数据(实例中断言的是meta这个子字典,预期结果也是写的meta子字典中的内容)
  response: $.meta

file_path:
	# 测试用例数据地址
  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用例展示

Snipaste_2020-08-03_15-30-29.png

脚本一览

#!/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):
        pass

    # 请求
    def base_requests(self, method, url, data=None, file_var=None, file_path=None, header=None):
        """

        :param method: 请求方法
        :param url: 接口path
        :param data: 数据,请传入dict样式的字符串
        :param file_path: 上传的文件路径
        :param file_var: 接口中接收文件对象的参数名
        :param header: 请求头
        :return: 完整的响应对象
        """
        session = requests.Session()
        if (file_var in [None, '']) and (file_path in [None, '']):
            files = None
        else:
            # 文件不为空的操作
            files = {file_var: open(file_path, 'rb')}
        # get 请求参数传递形式 params
        if method == 'get':
            res = session.request(method=method, url=url, params=data, headers=header)
        else:
            res = session.request(method=method, url=url, data=data, files=files, headers=header)
        logger.info(f'请求方法:{method},请求路径:{url}, 请求参数:{data}, 请求文件:{files}, 请求头:{header})')
        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):
        self.excel_file = excel_path
        self.book = xlrd.open_workbook(self.excel_file)

    def get_data(self):
        """

        :return:
        """
        data_list = []
        title_list = []

        table = self.book.sheet_by_index(0)
        for norw in range(1, table.nrows):
            # 每行第4列 是否运行
            if table.cell_value(norw, 3) == '否':
                continue
            # 每行第3列, 标题单独拿出来
            title_list.append(table.cell_value(norw, 1))

            # 返回该行的所有单元格组成的数据 table.row_values(0) 0代表第1列
            case_number = table.cell_value(norw, 0)
            path = table.cell_value(norw, 2)
            is_token = table.cell_value(norw, 4)
            method = table.cell_value(norw, 5)
            file_var = table.cell_value(norw, 6)
            file_path = table.cell_value(norw, 7)
            dependent = table.cell_value(norw, 8)
            data = table.cell_value(norw, 9)
            expect = table.cell_value(norw, 10)
            actual = table.cell_value(norw, 11)
            value = [case_number, path, is_token, method, file_var, file_path, dependent, data, expect, actual]
            logger.info(value)
            # 配合将每一行转换成元组存储,迎合 pytest的参数化操作,如不需要可以注释掉 value = tuple(value)
            value = tuple(value)
            data_list.append(value)
        return data_list, title_list

    def write_result(self, case_number, result):
        """

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

    # 读实际的响应
    def read_actual(self, depend):
        """

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

        pytest.main(args=[f'--alluredir={report_data}'])
        # # 启动一个web服务的报告
        # os.system('allure serve ./report/data')
        os.system(f'allure generate {report_data} -o {report_generate} --clean')
        logger.debug('报告已生成')

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

    @pytest.mark.parametrize('case_number,path,is_token,method,file_var,'
                             'file_path,dependent,data,expect,actual', data_list, ids=title_ids)
    def test_main(self, case_number, path, is_token, method, file_var, file_path,
                  dependent, data, expect, actual):

        with allure.step("处理相关数据依赖,header"):
            data, header = self.treating_data(is_token, dependent, data)
        with allure.step("发送请求,取得响应结果的json串"):
            res = br.base_requests(method=method, url=base_url + path, file_var=file_var, file_path=file_path,
                                   data=data, header=header)
        with allure.step("将响应结果的内容写入用例中的实际结果栏"):
            ReadData(case_data_path).write_result(case_number, res)

            # 写token的接口必须是要正确无误能返回token的
            if is_token == '写':
                with allure.step("从登录后的响应中提取token到header中"):
                    token_header['Authorization'] = jsonpath.jsonpath(res, token_reg)[0]
            logger.info(f'token_header: {token_header}, \n no_token_header: {no_token_header}')
        with allure.step("根据配置文件的提取响应规则提取实际数据"):
            really = jsonpath.jsonpath(res, res_reg)[0]
        with allure.step("处理读取出来的预期结果响应"):
            expect = eval(expect)
        with allure.step("预期结果与实际响应进行断言操作"):
            assert really == expect
            logger.info(f'完整的json响应: {res}\n 需要校验的数据字典: {really}\n 预期校验的数据字典: {expect}\n 测试结果: {really == expect}')


if __name__ == '__main__':
    from tools.zip_file import zipDir
    from tools.send_email import send_email
    t1 = TestApiAuto()
    t1.start_run_test()
    zipDir(report_generate, report_zip)
    send_email(email_setting)

运行结果

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

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

源码地址

源码地址: https://gitee.com/zy7y/apiAutoTest.git

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!