pytest+allure+yaml+jenkins接口自动化

目录

pytest推荐

allure推荐

jenkins

ok,我们开始

框架架构

 config

log.py

smt.py

data层

data.yaml

methods层

common.py

get_yaml.py

http_client.py

report报告层

html

xml

用例运行过程中生成的json数据存储的地方

request_list层

TY-login.py

test_case层

Basic_data

global_path.py

pytest.ini

main.py

下面我们看jenkins如何可以持续化配置

 配置代码根目录

 配置定时任务

​编辑

构建

 构建后的操作

 保存,运行。

生成后的allure报告


 

pytest推荐

  • 简单灵活
  • 支持参数化配置 -v -s -k 等
  • 支持ini文件
  • 支持conftest
  • 与requests配合做单元测试,场景测试都很ok

allure推荐

  • 可视化界面报告
  • 接口耗时、响应时间、成功、失败
  • 总之就是可以一眼看到测试用例的运行情况及报错信息,快速分析定位出用例问题

jenkins

  • jenkins就不多讲了,主要用与持续化,定时运行,邮件发送等,总之非常强大

 

ok,我们开始

fb2baa214c1b44dd820e22c9c533b4d0.jpeg

框架架构

e16a26030ddf47eb9f496c8f677406d3.png

 config

config层主要是封装了一些邮件发送的功能及日志功能,本文章没有用到smt.py文件,邮件发送走jenkins发送

log.py

日志封装

import logging
import time
from logging.handlers import TimedRotatingFileHandler
from global_path import *
from methods.common import *


def loger(level='DEBUG'):
    level = str(level).upper()
    #定义收集器
    logger = logging.getLogger('test')
    #配置收集器等级
    logger.setLevel(level)

    #配置控制台打印
    stearm_log = logging.StreamHandler()
    stearm_log.setLevel(level)

    #配置日志收集器--以时间维度进行统计
    filelog_name = fr'{log_path}\\{log_time()}.log'
    """
       #实例化TimedRotatingFileHandler
       # filename:日志文件名
       # when:日志文件按什么切分。'S'-秒;'M'-分钟;'H'-小时;'D'-天;'W'-周
       #       这里需要注意,如果选择 D-天,那么这个不是严格意义上的'天',是从你
       #       项目启动开始,过了24小时,才会重新创建一个新的日志文件,如果项目重启,
       #       这个时间就会重置。选择'MIDNIGHT'-是指过了凌晨12点,就会创建新的日志
       # interval是时间间隔
       # backupCount:是保留日志个数。默认的0是不会自动删除掉日志。如果超过这个个数,就会自动删除  
       """
    file_log = TimedRotatingFileHandler(filelog_name,
                                        when='MIDNIGHT',
                                        interval=1,
                                        backupCount=3)
    file_log.setLevel(level)
    #打印格式
    formatter = '[%(asctime)s] [%(threadName)s] [line:%(lineno)d] %(levelname)s: %(message)s'
    log_fmt = logging.Formatter(formatter)

    #配置打印格式
    stearm_log.setFormatter(log_fmt)
    file_log.setFormatter(log_fmt)

    #配置打印器追加到容器
    logger.addHandler(stearm_log)
    logger.addHandler(file_log)
    return logger

if __name__ == '__main__':
    a = loger('info')
    a.info('测试日志')

 

smt.py

邮件模块

# smtplib 用于邮件的发信动作
import smtplib
# email 用于构建邮件内容
from email.mime.text import MIMEText
# 构建邮件头
from email.header import Header


# 发信方的信息:发信邮箱,QQ 邮箱授权码
from_addr = '1798830003@qq.com'
password = '*****'
# 收信方邮箱
to_addr = '1798830003@qq.com'
# 发信服务器
smtp_server = 'smtp.qq.com'

# 邮箱正文内容,第一个参数为内容,第二个参数为格式(plain 为纯文本),第三个参数为编码
html_msg = """
<p>Python 邮件发送HTML格式文件测试...</p>
<p><a href="生成allure报告的url即可">这是一个链接</a></p>
"""
msg = MIMEText(html_msg, 'html', 'utf-8')

# 邮件头信息
msg['From'] = Header('张三')  # 发送者
msg['To'] = Header('李四')  # 接收者
subject = 'Python SMTP 邮件测试'
msg['Subject'] = Header(subject, 'utf-8')  # 邮件主题

try:
    smtpobj = smtplib.SMTP_SSL(smtp_server)
    # 建立连接--qq邮箱服务和端口号(可百度查询)
    smtpobj.connect(smtp_server, 465)
    # 登录--发送者账号和口令
    smtpobj.login(from_addr, password)
    # 发送邮件
    smtpobj.sendmail(from_addr, to_addr, msg.as_string())
    print("邮件发送成功")
except smtplib.SMTPException:
    print("无法发送邮件")
finally:
    # 关闭服务器
    smtpobj.quit()


data层

主要是针对环境数据及基础数据维护的yaml文件,针对yaml文件变量读取,我在methodsget_yaml.py中封装了对应的方法

test为测试环境

uat为生产环境

data.yaml

environment: uat

test:
  username: xxx
  password: xxx
  Authorization: xxx
  loginurl: xxx
  device: xxx
  xxx: xxx



uat:
  username: xxx
  password: xxx
  loginurl: xxx
  device: xxx
  xxx: xxx

methods层

主要封装一些公共方法,方便用例层,接口层调用

common.py

公共方法(当前时间、MD5加密、随机数)等,可封装在此

import time, hashlib, string, random
from jsonpath import jsonpath


def data_time():
    '''
    当前时间
    :return:
    '''
    local_time = time.strftime('%Y-%m-%d %H:%M:%S')
    return local_time


def time_code():
    '''
    当前时间
    :return:
    '''
    local_time = time.strftime('%Y%m%d%H%M%S')
    return local_time


def log_time():
    '''
    日志时间生成器
    :return:
    '''
    log_time = time.strftime('%Y-%m-%d')
    return log_time


def MD5_decode(pwd):
    '''
    解码
    :param pwd: 密码
    :return:
    '''
    m = hashlib.md5()
    m.update(pwd.encode('utf-8'))
    return m.hexdigest()


def random_int(count):
    '''
    生成数值、英文的随机数
    :param count: 生成的长度
    :return:
    '''
    int_num = string.digits
    english_num = string.ascii_uppercase
    num = random.sample(int_num + english_num, count)
    # print(num)
    znum = ''.join(num)
    print(f'生成随机数的结果为>>: {znum}')
    return znum


def list_dict(list1: list, list2: list):
    '''
    2列表转json
    :param list1:
    :param list2:
    :return:
    '''
    dict_list = dict(zip(list1, list2))
    return dict_list


def jsonpath_draw(dict_list: dict, key=None, twokey=None):
    if key == None:
        vlaue = jsonpath(dict_list, f'$.{twokey}')
        return vlaue
    else:
        value = jsonpath(dict_list, f'$.{key}.{twokey}')
        return value

get_yaml.py

根据不同的环境,获取yaml文件中的对应的数据

import yaml
from global_path import *
from config.log import *


def yaml_data():
    with open(datayaml_path, 'r', encoding='utf-8') as f:
        data = yaml.safe_load(f)
        if data['environment'] == 'test':
            data = data['test']
        elif data['environment'] == 'uat':
            data = data['uat']
        return data

http_client.py

基于request做了封装调用,并加上日志数据。方便调试与问题排查

import requests
import hashlib
import traceback
import json
from config.log import loger


class HttpClient(object):
    def __init__(self):
        self.session = requests.Session()
        self.log = loger('info')

    def request(self, requestMethod, requesturl, paramMethod=None, requestData=None, headers=None, **kwargs):
        try:
            if requestMethod.lower() == "post":
                self.log.info(f'\n请求格式 {str(requestMethod)}\n' +
                              f'请求url:{str(requesturl)}\n' +
                              f'传参类型:{str(paramMethod)}\n' +
                              f'body: {str(requestData)}\n' +
                              f'头部: {str(headers)}\n')
                return self.__post(requesturl=requesturl, paramMethod=paramMethod, requestData=requestData,
                                   headers=headers, **kwargs)
            elif requestMethod.lower() == "get":
                self.log.info(f'\n请求格式 {str(requestMethod)}\n' +
                              f'请求url:{str(requesturl)}\n' +
                              f'传参类型:{str(paramMethod)}\n' +
                              f'body {str(requestData)}\n' +
                              f'头部 {str(headers)}\n')
                return self.__get(requesturl, requestData=requestData, headers=headers, **kwargs)
            elif requestMethod.lower() == "patch":
                self.log.info(f'\n请求格式 {str(requestMethod)}\n' +
                              f'请求url:{str(requesturl)}\n' +
                              f'传参类型:{str(paramMethod)}\n' +
                              f'body {str(requestData)}\n' +
                              f'头部 {str(headers)}\n')
                return self.__patch(requesturl, paramMethod, requestData=requestData, headers=headers, **kwargs)
        except Exception as e:
            print(traceback.format_exc())

    def __post(self, requesturl, paramMethod, requestData=None, headers=None, **kwargs):
        try:
            if paramMethod == "form" or paramMethod == "data":
                responseObj = self.session.post(url=requesturl, data=requestData, headers=headers, **kwargs)
                self.log.info(f'--结果-- {str(responseObj.json())}')
                return responseObj
            elif paramMethod == 'json':
                responseObj = self.session.post(url=requesturl, json=requestData, headers=headers, **kwargs)
                self.log.info(f'--结果-- {str(responseObj.json())}')
                return responseObj
            elif paramMethod == 'files':
                responseObj = self.session.post(url=requesturl, files=requestData, headers=headers, **kwargs)
                self.log.info(f'--结果-- {str(responseObj.json())}')
                return responseObj
        except Exception as e:
            print(traceback.format_exc())

    def __get(self, requestUrl, requestData=None, headers=None, **kwargs):

        try:
            if requestData:
                responseObj = self.session.get(url=requestUrl, data=requestData, headers=headers, **kwargs)
                self.log.info(f'响应结果 {str(responseObj.status_code)}')
                self.log.info(f'--结果-- {str(responseObj.json())}')
                return responseObj
            else:
                responseObj = self.session.get(url=requestUrl, headers=headers, **kwargs)
                self.log.info(f'响应结果 {str(responseObj.status_code)}')
                self.log.info(f'--结果-- {str(responseObj.json())}')
                return responseObj
        except Exception as e:
            print(traceback.format_exc())

    def __patch(self, requesturl, paramMethod, requestData=None, headers=None, **kwargs):
        try:
            if paramMethod == "json" or paramMethod == "data":
                responseObj = self.session.patch(url=requesturl, data=requestData, **kwargs)
                self.log.info(f'响应结果 {str(responseObj.status_code)}')
                self.log.info(f'--结果-- {str(responseObj.json())}')
                return responseObj
        except Exception as e:
            print(traceback.format_exc())

report报告层

html

html生成的路径位置

0d49f0abd8194a6384df508d00994f6d.png

 

xml

8d6ecee8ea8b4d35b9c70cf4e72bc899.png

 

用例运行过程中生成的json数据存储的地方

request_list层

接口层,根据业务需要整理对应的接口,可以以业务模块进行分py,也可以根据场景进行分py

作者是根据系统模块层级分的py文件

TY-login.py

就拿本次项目的登录做讲解,登录py主要是获取系统中的token,拿到token后将token信息传递给其他接口使用

userdata=yaml_data()  获取get_yaml中yaml_data拿到的yaml信息

self.http = HttpClient()  封装接口方法调用
self.url = userdata['loginurl']  环境的url 如:测试环境为“xxx.dataserver.cn”   生产环境:“xxxx.baidu.com”

from methods.http_client import HttpClient
from methods.common import MD5_decode
from config.log import loger
from methods.get_yaml import yaml_data


class TY_testlogin():
    '''
    登录 台一
    '''

    def __init__(self):
        userdata = yaml_data()
        self.http = HttpClient()
        self.url = userdata['loginurl']

    def token(self, userdata):
        '''
        获取token
        :param userdata:
        :return:
        '''
        url = self.url + 'certification/token'
        data = {'username': userdata['username'],
                'password': userdata['password'],
                'grant_type': 'rsa_password'}
        headers = {'Authorization': userdata['Authorization']}

        res = self.http.request(requestMethod='post',
                                paramMethod='form',
                                requesturl=url,
                                requestData=data,
                                headers=headers)
        # print(res.status_code)
        assert 201 == res.status_code
        TY_token = res.json()['data']['access_token']
        return TY_token

TY-requestlist.py

基础数据业务接口,主要依赖login中获取的token数据。给业务接口使用

        token = TY_testlogin().token(userdata)
        self.header = {'Authorization': f'bearer {token}'}

# coding=utf-8
from methods.http_client import HttpClient
from methods.get_yaml import yaml_data
from request_list.TY_login import TY_testlogin


class Basic_data:
    '''
    基础数据
    '''

    def __init__(self):
        self.http = HttpClient()
        userdata = yaml_data()
        self.url = userdata['loginurl']
        token = TY_testlogin().token(userdata)
        self.header = {'Authorization': f'bearer {token}'}


    def finished_product(self, code, name, remarks=None):
        """
        新增产成品类型
        :param code: 编号
        :param name: 名称
        :param remarks: 备注
        :return:
        """
        url = self.url + '/instances'
        data = {
            "objectType": "ZYWL_BASIC_FINISHED_PRODUCT_TYPE",
            "identifier": code,
            "name": name,
            "description": remarks
        }

        res = self.http.request(requestMethod='post',
                                paramMethod='json',
                                requesturl=url,
                                requestData=data,
                                headers=self.header)
        print(type(res.status_code))
        assert 200 == res.status_code
        return res.json()

    def search_product(self, code):
        '''
        搜索产成品类型
        :param code: 编号
        :return:
        '''
        url = self.url + /datalinks'
        data = {
            "linkIds": [
                "1563456803885420544"
            ],
            "params": {
                "page": 1,
                "pageSize": 10000,
                "searchName": "",
                "searchNumber": code
            }
        }

        res = self.http.request(requestMethod='post',
                                paramMethod='json',
                                requesturl=url,
                                requestData=data,
                                headers=self.header)
        assert 200 == res.status_code
        return res.json()

    def delete_product(self, id):
        '''
        删除产成品类型
        :param id: id
        :return:
        '''
        url = self.url + 'api/batch?profileIdentifier=PRODUCT_TYPE'
        data = {
            "data": [
                id
            ],
            "method": "delete"
        }

        res = self.http.request(requestMethod='post',
                                paramMethod='json',
                                requesturl=url,
                                requestData=data,
                                headers=self.header)
        assert 200 == res.status_code
        return res.json()

test_case层

用例层,用例场景的集合,使用alluer结合pytest编写

Basic_data

  • 主要就是根据业务场景使用allure定义具体的步骤及用common中封装的一些通用方法,生成一些随机数,作为数据支撑完成测试场景的接口自动化
import pytest, allure
from global_path import *
from methods.common import time_code
from request_list.TY_login import TY_testlogin
from methods.get_yaml import yaml_data
from request_list.TY_requestlist import Basic_data


@allure.feature('基础数据')
class Test_old():

    def setup_class(self):
        print('-----用例开始------')
        self.Basic_data = Basic_data()

    @allure.story('产成品类型')
    def test_add_finished_product(self):
        allure.dynamic.title('新增')
        with allure.step('新增'):
            add = self.Basic_data.finished_product(code=f'auto{time_code()}', name='auto产成品', remarks=None)
            result = add['message']
            allure.attach(result, name='新增产成品结果', attachment_type=allure.attachment_type.TEXT)
            assert result == '操作成功'

    @allure.story('产成品类型')
    def test_search_finished_product(self):
        allure.dynamic.title('搜索')
        with allure.step('搜索'):
            search = self.Basic_data.search_product(code='auto')
            result = search[0]['data'][-1][2]
            allure.attach(result, name='搜索产成品结果', attachment_type=allure.attachment_type.TEXT)
            assert 'auto' in result

global_path.py

  • 项目路径层,定义每个包,在项目中的路径,方便代码调用
import os


#项目的根目录
project_path = os.path.dirname(__file__)

#根据项目根目录追加子目录
config_path = os.path.join(project_path,'config')

data_path = os.path.join(project_path,'data')

methods_path = os.path.join(project_path,'methods')

test_case_path = os.path.join(project_path,'test_case')

report_path = os.path.join(project_path,'report')

request_list_path = os.path.join(project_path,'request_list')

log_path = os.path.join(project_path,'log日志')

html_path = report_path+r'\html'

xml_path = report_path+r'\xml'

datayaml_path = data_path+r'\data.yaml'

pytest_path = project_path+r'\pytest.ini'

pytest.ini

  • 编写ini文件,定义需要那个文件包下的那些文件,执行的参数是什么
  • testpaths就是文件夹
  • python_files就是那些文件test_* 就是以test_开头的所有文件
  • addopts就是pytest运行时的参数
[pytest]


testpaths=./test_case
python_files=test_*

addopts= -vs --reruns=1 --clean-alluredir --alluredir=./report/xml

main.py

总运行入口

import os
import pytest
from global_path import *


if __name__ == '__main__':
    '''
    pytest运行参数在pytest.ini文件中配置
    '''
    pytest.main()
    os.system(f'allure generate {xml_path} -o {html_path} --clean')

至此整个接口自动化框架就编写完成了。

下面我们看jenkins如何可以持续化配置

79d7d5b3d9e341c0a438ed03e3ce511e.gif

263a0a95c5c54315bb9deb98d442edef.png

 配置代码根目录

c3a66abcdcae48a2a8d0bfc9ad2092b4.png

 配置定时任务

*   *   *   *   *
(五颗星,中间用空格隔开)

第一颗 * 表示分钟,取值0~59
第二颗 * 表示小时,取值0~23
第三颗 * 表示一个月的第几天,取值1~31
第四颗 * 表示第几月,取值1~12
第五颗 * 表示一周中的第几天,取值0~7,其中0和7代表的都是周日

1.每30分钟构建一次:H/30 * * * *
2.每2个小时构建一次:H H/2 * * *
3.每天早上8点构建一次:0 8 * * *
4.每天的8点,12点,22点,一天构建3次:0 8,12,22 * * *
(多个时间点,中间用逗号隔开)

f8a7f6c4cd19463cbbaa38f1fc2d79bd.png

构建

使用cmd命令 python main.py运行总开关文件

73b809edaf494b6e8717fc6eca78a057.png

 构建后的操作

3b4f07bafa2e48afbaf299e2b179d997.png

 保存,运行。

31dbb3aae93d4d15958b8ae3bfddf41f.png

 

生成后的allure报告

29fc3a8e6bf244e38b9b02068028c133.png

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值