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

项目说明

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

项目结构说明

项目构造图

结构介绍

1、api 用于定义各个服务下的不同接口
2、config 用于存放配置文件。
3、public 用于存放公共方法模块。
4、testcase 用于存放测试用例。
5、testdata 用于存放测试接口相关信息。
6、venv 虚拟环境,这个需要本地自动生成。
7、extral 资源文件,如更换头像所需要的图片文件等。
8、新引入的第三方包请补充到requirements.txt

项目功能

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

代码设计与功能说明

工具类封装

log 日志

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

// # coding=utf-8
import sys

import logbook
import os
from logbook import *
from logbook.more import ColorizedStderrHandler

# sys.path.append('../')
# sys.path.append('/Users/dasouche/')
#当前文件的上一层
curPath = os.path.abspath(os.path.dirname(__file__))
# 获取myProject,也就是项目的根路径
rootPath = curPath[:curPath.find("xiaobang-api-auto/")+len("xiaobang-api-auto/")]  # 获取myProject,也就是项目的根路径

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 = rootPath + '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_logger = Logger("global_log")


def init_logger():
    logbook.set_datetime_format("local")
    run_logger.handlers = []
    run_logger.handlers.append(log_file)
    run_logger.handlers.append(log_std)

'''
日志等级:
critical    严重错误,会导致程序退出
error	    可控范围内的错误
warning	    警告信息
notice	    大多情况下希望看到的记录
info	    大多情况不希望看到的记录
debug	    调试程序时详细输出的记录
'''
# 实例化,默认调用
# 初始化日志系统(被默认调用)
init_logger()

# if __name__ == "__main__":
#     run_logger.info("测试日志模块")
#     run_logger.info("测试")
#     run_logger.debug('sss')

也可以以。ini 文件配置代码片段如下

[loggers]
keys = root

[handlers]
keys = consoleHandler,fileHandler

[formatters]
keys = fmt

[logger_root]
level = DEBUG
handlers = consoleHandler,fileHandler

[handler_consoleHandler]
class = StreamHandler
level = DEBUG
formatter = fmt
args = (sys.stdout,)

[handler_fileHandler]
class = logging.handlers.RotatingFileHandler
level = DEBUG
formatter = fmt
args = ('%(logfilename)s', 'a', 10485760, 20)
[formatter_fmt]
format = %(asctime)s -- %(levelname)s -- %(name)s -- %(funcName)s -- %(lineno)d -- %(message)s
日志加载代码
config_log_file = os.path.join(config_dir, 'logging.ini')
if not os.path.exists(log_dir):
    os.makedirs(log_dir)
logpath = os.path.join(log_dir, '%s%s.log' % ('log-',time.strftime('%Y-%m-%d')))
logging.config.fileConfig(config_log_file,defaults={'logfilename':logpath})

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

# coding=utf-8
import os
import configparser

import conftest

curPath = os.path.abspath(os.path.dirname(__file__))
rootPath = curPath[:curPath.find("xiaobang-api-auto/")+len("xiaobang-api-auto/")]  # 获取myProject,也就是项目的根路径
# from util.dong.handle_log import run_logger as logger

logger = conftest.get_my_logger(os.path.basename(__file__))

class HandleInit:
    # 读取配置文件
    def load_ini(self):
        file_path = rootPath + "/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


handle_ini = HandleInit()
# if __name__ == "__main__":
#     he =  HandleInit()
#     print(he.get_value("id_token","token"))


3、Api 接口请求
  获取相关测试用例及接口用例配置
  handle_apirequest.py 部分代码

  # coding:utf-8
import os
import allure

import conftest

curPath = os.path.abspath(os.path.dirname(__file__))
rootPath = curPath[:curPath.find("xiaobang-api-auto/")+len("xiaobang-api-auto/")]  # 获取myProject,也就是项目的根路径
from api.base_request import baseRequest
#from util.dong.handle_log import run_logger as logger

logger = conftest.get_my_logger(os.path.basename(__file__))

class ApiRequest:
    def api_request(self, base_url, test_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_data['config']['name']
            get_url = base_url + test_data['config']['url']
            get_method = test_data['config']['method']
            get_headers = test_data['config']['headers']
            get_cookies = test_data['config']['cookies']
        except Exception as e:
            logger.exception('获取json数据失败,{}'.format(e))
        try:
            get_case_name = case_data['name']
            get_case_params = case_data['params']
        except Exception as e:
            logger.exception('获取case_data的json数据失败,{}'.format(e))
        with allure.step("请求地址:%s,请求方法:%s,请求头:%s,请求Cookies:%s" % (
                 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,请求Cookies:%r' % (get_url, get_method, get_headers, get_cookies))
        logger.info('请求接口用例名:%r,接口用例请求参数:%r' % (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' % (get_case_name, response_data.json()))
        return response_data


apiRequest = ApiRequest()

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

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import xlrd
from xlutils.copy import copy
import os


curPath = os.path.abspath(os.path.dirname(__file__))

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)

    # 写入数据
    def write_value(self, row, col, value):
        '''
        写入excel数据
        row,col,value
        '''
        read_data = xlrd.open_workbook(self.file_name)
        write_data = copy(read_data)
        sheet_data = write_data.get_sheet(0)
        sheet_data.write(row, col, value)
        write_data.save(self.file_name)

    # 根据对应的caseid 找到对应行的内容
    def get_rows_data(self, case_id):
        row_num = self.get_row_num(case_id)
        rows_data = self.get_row_values(row_num)
        return rows_data

    # 根据对应的caseid找到对应的行号
    def get_row_num(self, case_id):
        num = 0
        clols_data = self.get_cols_data()
        for col_data in clols_data:
            if case_id in col_data:
                return num
            num = num + 1

    # 根据行号,找到该行的内容
    def get_row_values(self, row):
        tables = self.data
        row_data = tables.row_values(row)
        return row_data

    # 获取某一列的内容
    def get_cols_data(self, col_id=None):
        if col_id != None:
            cols = self.data.col_values(col_id)
        else:
            cols = self.data.col_values(0)
        return cols


if __name__ == '__main__':
    opers = OperationExcel()
    print(opers.get_cell_value(1, 2))

5、Json 数据处理
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"
                }
            ]
        }
    ]
}

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

# coding=utf-8
import sys
import json
import os

curPath = os.path.abspath(os.path.dirname(__file__))
rootPath = curPath[:curPath.find("xiaobang-api-auto/")+len("xiaobang-api-auto/")]
baseFileName = rootPath + 'testdata/business_end/payCreateactive.json'

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

    def getJson_values(self, key, file_name):
        if file_name == None:
            return ""
        jsonData = self.load_json(file_name)
        if key == None:
            getJsonValues = ""
        else:
            for x in jsonData :
               if x.get('keyword') == key :
                   getJsonValues = x
                   break
               else:
                   getJsonValues = ''
        return getJsonValues


handle_jsonData = HandleJson()
# if __name__ == "__main__":
#     hjson = HandleJson()
#     params = hjson.load_json(baseFileName)
#     print(params)

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

# coding:utf-8
import os

import requests
import json
#from util.dong.handle_log import run_logger as logger
import conftest
from public.util.dong.handle_init import handle_ini

logger = conftest.get_my_logger(os.path.basename(__file__))


curPath = os.path.abspath(os.path.dirname(__file__))
rootPath = curPath[:curPath.find("xiaobang-api-auto/")+len("xiaobang-api-auto/")]
id_token = handle_ini.get_value("id_token","token")

class BaseRequest:
    get_data = None
    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, data=data, cookies=cookie, headers=header)
        return response

        # 主函数调用

    def run_main(self, method, url, data, header, cookie=None):
        header["ID-TOKEN"] = id_token
        get_contentType = header['Content-Type']
        try:
            result = ''
            if method.upper() == 'GET':
                result = self.send_get(url, data, header, cookie)
            elif method.upper() == 'POST':
                if get_contentType == 'multipart/form-data':
                    filePath = self.creat_filePath()
                    get_data = {'file': open(filePath, 'rb')}  # 一般使用来上传文件(较少用)
                elif get_contentType == 'application/json':
                    get_data = json.dumps(data)
                else:
                    get_data = data  # 默认 application/x-www-form-urlencoded、以form表单形式提交数据。
                result = self.send_post(url, get_data, header, cookie)
            return result
        except Exception as e:
            logger.exception('请求主函数调用失败:{}'.format(e))

    def creat_filePath(self):
        filePath = rootPath + '/extral' + '/testfile.testfile.text'
        if not os.path.exists(filePath):
            os.makedirs(filePath)
        return filePath
# 实例
baseRequest = BaseRequest()

# if __name__ == "__main__":

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


import os
import allure
import pytest

from public.utils import *
from util.dong.handle_apiRequest import apiRequest
from util.dong.handle_init import handle_ini
from util.dong.handle_json import handle_jsonData
from pactverify.matchers import PactVerify, Like, EachLike, Enum, Matcher

log = conftest.get_my_logger(os.path.basename(__file__))

curPath = os.path.abspath(os.path.dirname(__file__))
rootPath = curPath[:curPath.find("xiaobang-api-auto/")+len("xiaobang-api-auto/")]

baseFileName = rootPath + 'testdata/business_end/payController.json'
baseurl = handle_ini.get_value('apiurl', 'creatactive')
testCaseData = handle_jsonData.load_json(baseFileName)

@allure.feature('参加活动并查看订单')
class TestJoinActivity():
    @allure.story('创建活动')
    @pytest.mark.parametrize('case_data', testCaseData['testcase'])
    def test_creatActivity(self,case_data):
        # 活动的名字title生产随机值
        case_data["params"]["title"] = get_Unicode()
        # 开始时间区系统当前时间
        case_data["params"]["signStart"] = get_time_block()[0]
        # 结束时间当前时间+24小时
        case_data["params"]["signEnd"] = get_time_block()[1]
        api_response = apiRequest.api_request(baseurl, testCaseData, case_data)
        config_contract_format = Matcher({
            'msg': 'success',
            'code': '200',
            'success': True
        })
        mPactVerify = PactVerify(config_contract_format, hard_mode=False)
        try:
            mPactVerify.verify(api_response.json())
            log.info('verify_result:{},verify_info:{}'.format(mPactVerify.verify_result, mPactVerify.verify_info))
            assert mPactVerify.verify_result == True
        except Exception as e:
            log.exception('测试用例契约校验失败,verify_result:{},verify_info:{},exception:{}'.format(mPactVerify.verify_result,
                                                                                           mPactVerify.verify_info, e))
            raise Exception("契约校验失败,请求接口错误")


运行
  运用 Pytest 和 Allure 的特性,命令行运行测试用例文件夹,并生成对应的 allure 测试报告。
pytest.main([’-s’,’-q’,‘test_payCreateActive.py’,’–alluredir’, ‘…/…/report/xml’])
‘’’
第一个路径是指定上个main中生成的文件路径
第二个路径是指定生成HTML后存放的文件路径
–clean 是去除上次生成的信息,形成新的HTML
‘’’
os.system(“allure generate …/…/report/xml -o …/…/report/html --clean”)

Allure 测试报告
  当我们运行主函数时,并生成对应的测试用例报告时,我们可以看到在该文件夹中会生成对应的 json 文件的测试报告。将 json 文件的测试报告转换成 html 形式的。

  • 4
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值