接口测试 Python+Pytest+Allure+Jenkins 接口自动化框架

wuwei · 2020年08月14日 · 最后由 薛晓华 回复于 2024年03月22日 · 12814 次阅读

Python+Pytest+Allure+Jenkins 接口自动化框架

一、接口基础

  接口测试是对系统和组件之间的接口进行测试,主要是效验数据的交换,传递和控制管理过程,以及相互逻辑依赖关系。其中接口协议分为 HTTP,RPC,Webservice,Dubbo,RESTful 等类型。
  接口测试流程
1、需求评审,熟悉业务和需求
2、开发提供接口文档
3、编写接口测试用例
4、用例评审
5、提测后开始测试
6、提交测试报告
  两种常见的 HTTP 请求方法:GET 和 POST

二、项目说明

  本框架是一套基于 Python+Pytest+Requests+Allure+Jenkins 而设计的数据驱动接口自动化测试的框架。
技术栈
Python、Pytest、Requests、Excel、Json、Mysql、Allure、Logbook、Jenkins

三、接口测试框架结构图

四、项目功能

  Python+Pytest+Allure+Jenkins 接口自动化框架,实现 Excel 或 Json 维护测试用例,支持数据库操作,利用封装的请求基类调取相应的测试用例接口,获取配置文件中的环境地址与环境变量,结合 Pytest 进行单元测试,使用 LogBook 进行记录日志,并生成 allure 测试报告,最后进行 Jenkins 集成项目实现集成部署,并发送测试报告邮件。

五、代码设计与功能说明

1、工具类封装

1.1、log 日志
  目中的 log 日志是 logbook 进行日志记录的,方便测试开发调试时进行排错纠正或修复优化。日志可选择是否打印在屏幕上即运行时是否在终端输出打印。日志格式输出可调整。
  handle_log.py 部分源码

def log_type(record, handler):
    log = "[{date}] [{level}] [{filename}] [{func_name}] [{lineno}] {msg}".format(
        date=record.time,  # 日志时间
        level=record.level_name,  # 日志等级
        filename=os.path.split(record.filename)[-1],  # 文件名
        func_name=record.func_name,  # 函数名
        lineno=record.lineno,  # 行号
        msg=record.message  # 日志内容
    )
    return log
# 日志存放路径
LOG_DIR = BasePath + '/log'
print(LOG_DIR)
if not os.path.exists(LOG_DIR):
    os.makedirs(LOG_DIR)
# 日志打印到屏幕
log_std = ColorizedStderrHandler(bubble=True)
log_std.formatter = log_type
# 日志打印到文件
log_file = TimedRotatingFileHandler(
    os.path.join(LOG_DIR, '%s.log' % 'log'), date_format='%Y-%m-%d', bubble=True, encoding='utf-8')
log_file.formatter = log_type

# 脚本日志
run_log = Logger("global_log")
def init_logger():
    logbook.set_datetime_format("local")
    run_log.handlers = []
    run_log.handlers.append(log_file)
    run_log.handlers.append(log_std)
    return ""

  打印在终端的日志,如下图所示。

  同时运行项目后,会在项目文件 log 中自动生成一个以当天日期命名的 log 文件。点击 log 日志文件可查看日志详情即项目运行时所记录的日志或报错日志。如下图所示。

1.2、配置文件
  项目中涉及到一些配置文件如 username、password 或环境变量时,我们可通过配置文件来获取配置值。通过配置文件中 key 与 value 的定义来确定获取配置文件的值。
  handle_init.py 部分源码

class HandleInit:
    # 读取配置文件
    def load_ini(self):
        file_path = BasePath + "/config/config.ini"
        cf = configparser.ConfigParser()
        cf.read(file_path, encoding='UTF-8')
        return cf

    # 获取ini里面对应key的value
    def get_value(self, key, node=None):
        if node == None:
            node = 'Test'
        cf = self.load_ini()
        try:
            data = cf.get(node, key)
            logger.info('获取配置文件的值,node:{},key:{}, data:{}'.format(node, key, data))
        except Exception:
            logger.exception('没有获取到对应的值,node:{},key:{}'.format(node, key))
            data = None
        return data

  获取配置文件中的值日志如下图所示。

1.3、Api 接口请求
  获取相关测试用例及接口用例配置,记录请求相关参数的日志,定义 Allure 测试报告的步骤。
  handle_apirequest.py 部分代码

class ApiRequest:
    def api_request(self, base_url, test_case_data, case_data):
        get_name = None
        get_url = None
        get_method = None
        get_headers = None
        get_cookies = None
        get_case_name = None
        get_case_params = None
        response_data = None
        try:
            get_name = test_case_data['config']['name']
            get_url = base_url + test_case_data['config']['url']
            get_method = test_case_data['config']['method']
            get_headers = test_case_data['config']['headers']
            get_cookies = test_case_data['config']['cookies']
        except Exception as e:
            logger.exception('获取用例基本信息失败,{}'.format(e))
        try:
            get_case_name = case_data['name']
            get_case_params = case_data['params']
        except Exception as e:
            logger.exception('获取测试用例信息失败,{}'.format(e))
        with allure.step("请求接口:%s,请求地址:%s,请求方法:%s,请求头:%s,请求Cookies:%s" % (
                get_name, get_url, get_method, get_headers, get_cookies)):
            allure.attach("接口用例描述:", "{0}".format(get_case_name))
            allure.attach("接口用例请求参数:", "{0}".format(get_case_params))
        logger.info(
            '请求接口名:%r,请求地址:%r,请求方法:%r,请求头:%r,请求Cookies:%r' % (get_name, get_url, get_method, get_headers, get_cookies))
        logger.info('请求接口名:%r,请求接口用例名:%r,接口用例请求参数:%r' % (get_name, get_case_name, get_case_params))
        try:
            response_data = baseRequest.run_main(get_method, get_url, get_case_params, get_headers)
        except Exception as e:
            logger.exception('用例请求返回失败,{}'.format(e))
        logger.info('请求接口名:%r,请求接口用例名:%r,返回参数:%r' % (get_name, get_case_name, response_data.json()))
        return response_data

1.4、Excel 数据处理
1.4.1、Excel 测试用例

  测试用例中维护在 Excel 文件中,类中定义如何获取 Excel 中的相关数据(如获取某个单元格的内容,获取单元格的行数,以及将数据写入 Excel 中等操作)。
  handle_exceldata.py 部分源码

class OperationExcel:
    def __init__(self, file_name=None, sheet_id=None):
        if file_name:
            self.file_name = file_name
            self.sheet_id = sheet_id
        else:
            self.file_name = ''
            self.sheet_id = 0
        self.data = self.get_data()

    # 获取sheets的内容
    def get_data(self):
        data = xlrd.open_workbook(self.file_name)
        tables = data.sheets()[self.sheet_id]
        return tables

    # 获取单元格的行数
    def get_lines(self):
        tables = self.data
        return tables.nrows

    # 获取某一个单元格的内容
    def get_cell_value(self, row, col):
        return self.data.cell_value(row, col)

1.5、Json 数据处理
1.5.1、Json 测试用例

{
    "config":{
        "name":"post接口名",
        "url":"/langdetect",
        "method":"POST",
        "headers":{
            "Content-Type":"application/json"
        },
        "cookies":{

        }
    },
    "testcase":[
        {
            "name":"测试用例1",
            "params":{
                "query":"测试"
            },
            "validate":[
                {
                    "check":"status_code",
                    "comparator":"eq",
                    "expect":"200"
                }
            ]
        },
        {
            "name":"测试用例2",
            "params":{
                "query":"python"
            },
            "validate":[
                {
                    "check":"msg",
                    "comparator":"eq",
                    "expect":"success"
                }
            ]
        }
    ]
}

1.5.2、Json 用例处理
  获取 Json 文件中里具体字段的值。
  handle.json.py 部分源码

class HandleJson:
    # 读取json文件
    def load_json(self, file_name):
        if file_name == None:
            file_path = ""
        else:
            file_path = file_name
        try:
            with open(file_path, encoding='UTF-8') as f:
                data = json.load(f)
            return data
        except Exception:
            print("未找到json文件")
            return {}

    # 读取json文件里具体的字段值
    def getJson_value(self, key, file_name):
        if file_name == None:
            return ""
        jsonData = self.load_json(file_name)
        if key == None:
            getJsonValue = ""
        else:
            getJsonValue = jsonData.get(key)
        return getJsonValue

2、基类封装

2.1、请求基类封装
  接口支持 Get、Post 请求,调用 requests 请求来实现接口的调用与返回。接口参数包括,接口地址、接口请求参数、cookie 参数、header 参数。

class BaseRequest:

    def send_get(self, url, data, header=None, cookie=None):
        """
        Requests发送Get请求
        :param url:请求地址
        :param data:Get请求参数
        :param cookie:cookie参数
        :param header:header参数
        """
        response = requests.get(url=url, params=data, cookies=cookie, headers=header)
        return response

    def send_post(self, url, data, header=None, cookie=None):
        """
        Requests发送Post请求
        :param url:请求地址
        :param data:Post请求参数
        :param data:Post请求参数
        :param cookie:cookie参数
        :param header:header参数
        """
        response = requests.post(url=url, json=data, cookies=cookie, headers=header)
        return response

        # 主函数调用

    def run_main(self, method, url, data, header, cookie=None):
        try:
            result = ''
            if method.upper() == 'GET':
                result = self.send_get(url, data, header, cookie)
            elif method.upper() == 'POST':
                result = self.send_post(url, data, header, cookie)
            return result
        except Exception as e:
            logger.exception('请求主函数调用失败:{}'.format(e))

3、接口测试用例编写

3.1、接口测试用例
  引用 Pytest 来进行接口的单元测试,通过 JSON 中多个测试用例来做为参数化数据驱动。结合 Allure 制定相应接口的测试报告。在接口返回断言之前,我们先进行该接口的契约测试,我们采用的是 Pactverity 的全量契约校验测试。当契约测试通过时,我们再进行返回参数的相关校验测试。
  test_getRequestJson.py 部分源码

@allure.feature('测试GET请求模块')
class TestRequestOne():
    @allure.title('测试标题')
    @allure.testcase('测试地址:https://www.imooc.com')
    @user4ize('case_data', testCaseData['testcase'])
    def test_requestOne(self, case_data):
        try:
            api_response = apiRequest.api_request(baseurl, testCaseData, case_data)
            api_response_data = api_response.json()
            # pactverity——全量契约校验
            config_contract_format = Like({
                "msg": "成功",
                "result": 0,
                "data": EachLike({
                    "word": Like("testng")
                })
            })
            mPactVerify = PactVerify(config_contract_format)
            try:
                mPactVerify.verify(api_response_data)
                logger.info(
                    'verify_result:{},verify_info:{}'.format(mPactVerify.verify_result, mPactVerify.verify_info))
                assert mPactVerify.verify_result == True
            except Exception:
                err_msg = '契约校验错误'
                logger.exception('测试用例契约校验失败,verify_result:{},verify_info:{}'.format(mPactVerify.verify_result,
                                                                                     mPactVerify.verify_info))
            try:
                for case_validate in case_data['validate']:
                    logger.info('断言期望相关参数:check:{},comparator:{},expect:{}'.format(case_validate['check'],
                                                                                   case_validate['comparator'],
                                                                                   case_validate['expect']))
                    comparatorsTest.comparators_Assert(api_response, case_validate['check'],
                                                       case_validate['comparator'], case_validate['expect'])
                    logger.info('测试用例断言成功')
            except Exception as e:
                logger.exception('测试用例断言失败')
        except Exception as e:
            logger.exception('测试用例请求失败,原因:{}'.format(e))

3.2、主运行
  运用 Pytest 和 Allure 的特性,命令行运行测试用例文件夹,并生成对应的 allure 测试报告。

if __name__ == "__main__":
    pytest.main(['-s', '-v', 'test_case/testRequest/', '-q', '--alluredir', 'reports'])

4、Allure2 测试报告

  当我们运行主函数时,并生成对应的测试用例报告时,我们可以看到在该文件夹中会生成对应的 json 文件的测试报告。将 json 文件的测试报告转换成 html 形式的。命令如下
  reports 是 json 格式测试报告存放的目录位置,allure_reports 是 html 测试报告文件生成的目录位置。allure 命令如下。

  allure generate reports -o allure_result/

  项目根目录下的 allure_reports 文件,存放的是 allure 生成的测试报告。可看出文件下有一个 HTML 文件,可通过 Python 的编辑器 Pycharm 来打开该 HTML 文件(测试报告),或可通过 allure 命令来打开该 HTML,展示 HTML 测试报告。如下所示。
  测试报告文件,HTML 测试报告如下。

  allure 命令打开 HTML 测试报告。命令如下所示。

allure open allure_result/

  如下图所示。

  打开生成的 HTML 测试报告如下图所示。

5、Jenkins 集成

  Allure+Jenkins 的分享,我之前在 Pytest+Allure+Jenkins 的博客中已经分享过了。这块可以出门左转看看。前期的准备就不在这里重复说明了。我们就直接来上手创建 Item 进行相关配置。
  General 中 GitHub 项目地址的配置,将自己项目的 Git 复制至项目 URL 处。如下图所示。

  源码管理设置。勾选 Git,填写相应的项目 Git 地址,Git 项目权限所有者,以及对应的拉取代码的分支。如下图所示。

  配置构建命令。选择 “执行 windows 批处理命令”,用 python 运行主函数运行脚本,命令如下图所示。

  当我们在 Jenkins 里面成功安装 Allure 插件后,直接可以在构建后操作中配置 Allure 的相关配置。在 Pytest+Allure+Jenkins 中已经说明过的。Results 应与项目运行时设置的 Allure 生成的 Json 格式报告的路径一致,Report path 为 Allure html 报告结果生成文件存放的路径。

  排除万难之后,我们就可以用 Jenkins 来运行项目了。如下图所示。

  测试报告详情页,如下图所示。

六、后期优化

  1、接口测试用例数据依赖
  2、测试报告邮件的发送

七、感想

  该框架是在涉及 python 的知识点比较多,将接口测试与契约测试结合起来。该框架是在工作之余学习多篇文章,实战上手逐步入门开始的,适合新手入门接口自动化实战练习,仅供参考学习。框架中有不少可优化点与不足点,希望大家多多提建议或想法。

  开源地址:https://github.com/wuwei88/Apiautomation.git

共收到 17 条回复 时间 点赞

真滴强!留言 9999+

仅楼主可见

这个框架支持 rpc 接口测试吗

@ 剑玄 你的是什么 rpc?

不知道为啥会报这个错,是解析 json 数据问题么

================================== FAILURES ===================================
________________________ TestMainExcel.test_mainExcel _________________________

self =

@allure.title('测试标题')
@allure.testcase('测试地址:https://www.imooc.com')
def test_mainExcel(self):
excelData = GetData(file_name, sheet_id)
rows_count = excelData.get_case_lines()
for i in range(1, rows_count):
is_run = excelData.get_is_run(i)
if is_run:
url = excelData.get_request_url(i)
method = excelData.get_request_method(i)
request_data = json.loads(excelData.get_request_data(i))
header = json.loads(handle_ini.get_value('headerDefault', 'header'))
expect = excelData.get_expcet_data(i)
with allure.step('接口请求信息:'):
allure.attach('接口名:{},接口请求地址:{},接口请求方式:{},接口请求参数:{}'.format(excelData.get_name(i), url, method, request_data))
res = baseRequest.run_main(method, url, request_data, header)

res_data = res.json()

test_case\testRequest\test_mainExcel.py:40:


venv\lib\site-packages\requests\models.py:898: in json
return complexjson.loads(self.text, **kwargs)
E:\python3.6.2\lib\json_init_.py:354: in loads
return _default_decoder.decode(s)
E:\python3.6.2\lib\json\decoder.py:339: in decode
obj, end = self.raw_decode(s, idx=_w(s, 0).end())


self =
s = 'jsonpcallback({"result":0,"data":"","msg":"\u6210\u529f"})', idx = 0

def raw_decode(self, s, idx=0):
"""Decode a JSON document from s (a str beginning with
a JSON document) and return a 2-tuple of the Python
representation and the index in s where the document ended.

This can be used to decode a JSON document from a string that may
have extraneous data at the end.

"""
try:
obj, end = self.scan_once(s, idx)
except StopIteration as err:

raise JSONDecodeError("Expecting value", s, err.value) from None
E json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

E:\python3.6.2\lib\json\decoder.py:357: JSONDecodeError
=========================== short test summary info ===========================
FAILED test_case/testRequest/test_mainExcel.py::TestMainExcel::test_mainExcel
=================== 1 failed, 4 passed in 217.25s (0:03:37) ===================

Process finished with exit code 0

仅楼主可见

地址打不开 报 404

仅楼主可见

大佬 github 的地址 404 了。请问还能看吗?

楼主,能分享下代码不

时间久了,还有代码共享不

赞 ~ 文章内容很完整,可以借鉴一下 😀

源码可以分享一下吗?谢谢楼主

源码地址打不开了,作者可以给一下吗

源码能共享么

@wuwei88 楼主,请问还可以分享源码吗?

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册