python+solicitudes+pytest+marco de automatización allure

1. Biblioteca principal

solicitudes  solicitud solicitud

operación de archivo openpyxl  excel

iniciar sesión iniciar sesión

smtplib envía correo electrónico

analizador de configuración

servicio simulado unittest.mock

2. Estructura del directorio

base

utiles

datos de prueba

confinar

Casos de prueba

informe de prueba

registros

otro

imagen

2.1base

base_path.py almacena la ruta absoluta para evitar errores al ejecutar comandos DOS o Jenkins.

base_requests.py encapsula solicitudes, selecciona diferentes métodos para ejecutar scripts según el método y maneja excepciones de solicitudes al mismo tiempo

2.1.1 ruta_base.py

import os

# 项目根路径
_root_path = os.path.split(os.path.split(os.path.realpath(__file__))[0])[0]

# 报告路径
report_path = os.path.join(_root_path, 'testReport', 'report.html')

# 日志路径
log_path = os.path.join(_root_path, 'logs/')

# 配置文件路径
conf_path = os.path.join(_root_path, 'conf', 'auto_test.conf')

# 测试数据路径
testdatas_path = os.path.join(_root_path, 'testDatas')

# allure 相关配置
_result_path = os.path.join(_root_path, 'testReport', 'result')
_allure_html_path = os.path.join(_root_path, 'testReport', 'allure_html')
allure_command = 'allure generate {} -o {} --clean'.format(_result_path, _allure_html_path)

2.1.2 solicitudes_base.py

import json
import allure
import urllib3
import requests
import warnings
from bs4 import BeautifulSoup
from base.base_path import *
from requests.adapters import HTTPAdapter
from utils.handle_logger import logger
from utils.handle_config import handle_config as hc


class BaseRequests:

    def __init__(self, case, proxies=None, headers=None, cookies=None, timeout=15, max_retries=3):
        '''
        :param case: 测试用例
        :param proxies: The result is displayed in fiddler:
        {"http": "http://127.0.0.1:8888", "https": "https://127.0.0.1:8888"}
        :param headers: 请求头
        :param cookies: cookies
        :param timeout: 请求默认超时时间15s
        :param max_retries: 请求超时后默认重试3次
        '''
        self.case = case
        self.proxies = proxies
        self.headers = headers
        self.cookies = cookies
        self.timeout = timeout
        self.max_retries = max_retries
        self.base_url = hc.operation_config(conf_path, 'BASEURL', 'base_url')

    def get_response(self):
        '''获取请求结果'''
        response = self._run_main()
        return response

    def _run_main(self):
        '''发送请求'''
        method = self.case['method']
        url = self.base_url + self.case['url']
        if self.case['parameter']:
            data = eval(self.case['parameter'])
        else:
            data = None

        s = requests.session()
        s.mount('http://', HTTPAdapter(max_retries=self.max_retries))
        s.mount('https://', HTTPAdapter(max_retries=self.max_retries))
        urllib3.disable_warnings()  # 忽略浏览器认证(https认证)警告
        warnings.simplefilter('ignore', ResourceWarning)    # 忽略 ResourceWarning警告

        res=''
        if method.upper() == 'POST':
            try:
                res = s.request(method='post', url=url, data=data, verify=False, proxies=self.proxies, headers=self.headers, cookies=self.cookies, timeout=self.timeout)
            except Exception as e:
                logger.error('POST请求出错,错误信息为:{0}'.format(e))

        elif method.upper() == 'GET':
            try:
                res = s.request(method='get', url=url, params=data, verify=False,proxies=self.proxies, headers=self.headers, cookies=self.cookies, timeout=self.timeout)
            except Exception as e:
                    logger.error('GET请求出错,错误信息为:{0}'.format(e))
        else:
            raise ValueError('method方法为get和post')
        logger.info(f'请求方法:{method},请求路径:{url}, 请求参数:{data}, 请求头:{self.headers}, cookies:{self.cookies}')

        # with allure.step('接口请求信息:'):
        #     allure.attach(f'请求方法:{method},请求路径:{url}, 请求参数:{data}, 请求头:{headers}')

        # 拓展:是否需要做全量契约验证?响应结果是不同类型时,如何处理响应?
        return res


if __name__ == '__main__':
    # case = {'method': 'get', 'url': '/article/top/json', 'parameter': ''}
    case = {'method': 'post', 'url': '/user/login', 'parameter': '{"username": "xbc", "password": "123456"}'}
    response = BaseRequests(case).get_response()
    print(response.json())

2.2 utilidades

(Solo toma la parte central)

handle_excel.py

Operación de Excel, requisitos del marco, los datos de lectura finales deben guardarse en el formato de lista de diccionario anidado [{}, {}]
otras operaciones
handle_sendEmail.py

Python utiliza el protocolo SMTP para enviar correos electrónicos y pop3 para recibir correos electrónicos.
Debe habilitar la función de servicio pop3. La contraseña aquí es el código de autorización. Habilite el servicio usted mismo. Baidu
handle_logger.py procesamiento de registros


Procesamiento del archivo de configuración handle_config.py , aquí solo se puede configurar el nombre de dominio, solo cambie el nombre de dominio al cambiar de entorno

handle_allure.py
El informe generado por allure debe abrirse llamando a la línea de comando y el comando se encapsula directamente aquí.

handle_cookies.py (omitido)
se agrega en git para manejar objetos cookiesJar

handle_mock.py (omitido)
se agrega en git. El marco no se utiliza, pero también se encapsula en un método.

param_replace(omitido)

Encapsule operaciones parametrizadas de uso común en clases

2.2.1 handle_excel.py

import openpyxl
from base.base_path import *

class HandleExcel:
    def __init__(self, file_name=None, sheet_name=None):
        '''
        没有传路径时,默认使用 wanadriod接口测试用例.xlsx 文件
        :param file_name:  用例文件
        :param sheet_name: 表单名
        '''
        if file_name:
            self.file_path = os.path.join(testdatas_path, file_name)
            self.sheet_name = sheet_name
        else:
            self.file_path = os.path.join(testdatas_path, 'wanadriod接口测试用例.xlsx')
            self.sheet_name = 'case'
        # 创建工作簿,定位表单
        self.wb = openpyxl.load_workbook(self.file_path)
        self.sheet = self.wb[self.sheet_name]
        # 列总数,行总数
        self.ncols = self.sheet.max_column
        self.nrows = self.sheet.max_row

    def cell_value(self, row=1, column=1):
        '''获取表中数据,默认取出第一行第一列的值'''
        return self.sheet.cell(row, column).value

    def _get_title(self):
        '''私有函数, 返回表头列表'''
        title = []
        for column in range(1, self.ncols+1):
            title.append(self.cell_value(1, column))
        return title

    def get_excel_data(self):
        '''
        :return: 返回字典套列表的方式 [{title_url:value1, title_method:value1}, {title_url:value2, title_method:value2}...]
        '''
        finally_data = []
        for row in range(2, self.nrows+1):
            result_dict = {}
            for column in range(1, self.ncols+1):
                result_dict[self._get_title()[column-1]] = self.cell_value(row, column)
            finally_data.append(result_dict)
        return finally_data

    def get_pytestParametrizeData(self):
        '''
        选用这种参数方式,需要使用数据格式 列表套列表 @pytest.mark.parametrize('', [[], []]), 如 @pytest.mark.parametrize(*get_pytestParametrizeData)
        将 finally_data 中的 title 取出,以字符串形式保存,每个title用逗号(,)隔开
        将 finally_data 中的 value 取出,每行数据保存在一个列表,再集合在一个大列表内
        :return: title, data
        '''
        finally_data = self.get_excel_data()
        data = []
        title = ''
        for i in finally_data:
            value_list = []
            key_list = []
            for key, value in i.items():
                value_list.append(value)
                key_list.append(key)
            title = ','.join(key_list)
            data.append(value_list)
        return title, data

    def rewrite_value(self, new_value, case_id, title):
        '''写入excel,存储使用过的数据(参数化后的数据)'''
        row = self.get_row(case_id)
        column = self.get_column(title)
        self.sheet.cell(row, column).value = new_value
        self.wb.save(self.file_path)

    def get_row(self, case_id):
        '''通过执行的 case_id 获取当前的行号'''
        for row in range(1, self.nrows+1):
            if self.cell_value(row, 1) == case_id:
                return int(row)

    def get_column(self, title):
        '''通过表头给定字段,获取表头所在列'''
        for column in range(1, self.ncols+1):
            if self.cell_value(1, column) == title:
                return int(column)


if __name__ == '__main__':
    r = HandleExcel()
    print(r.get_excel_data())

2.2.2 handle_sendEmail.py

import smtplib
from utils.handle_logger import logger
from email.mime.text import MIMEText    # 专门发送正文邮件
from email.mime.multipart import MIMEMultipart  # 发送正文、附件等
from email.mime.application import MIMEApplication  # 发送附件

class HandleSendEmail:

    def __init__(self, part_text, attachment_list, password, user_list, subject='interface_autoTestReport', smtp_server='smtp.163.com', from_user='[email protected]', filename='unit_test_report.html'):
        '''
        :param part_text: 正文
        :param attachment_list: 附件列表
        :param password: 邮箱服务器第三方密码
        :param user_list: 收件人列表
        :param subject: 主题
        :param smtp_server: 邮箱服务器
        :param from_user: 发件人
        :param filename: 附件名称
        '''
        self.subject = subject
        self.attachment_list = attachment_list
        self.password = password
        self.user_list = ';'.join(user_list)    # 多个收件人
        self.part_text = part_text
        self.smtp_server = smtp_server
        self.from_user = from_user
        self.filename = filename

    def _part(self):
        '''构建邮件内容'''
        # 1) 构造邮件集合体:
        msg = MIMEMultipart()
        msg['Subject'] = self.subject
        msg['From'] = self.from_user
        msg['To'] = self.user_list

        # 2) 构造邮件正文:
        text = MIMEText(self.part_text)
        msg.attach(text)  # 把正文加到邮件体里面

        # 3) 构造邮件附件:
        for item in self.attachment_list:
            with open(item, 'rb+') as file:
                attachment = MIMEApplication(file.read())
            # 给附件命名:
            attachment.add_header('Content-Disposition', 'attachment', filename=item)
            msg.attach(attachment)

        # 4) 得到完整的邮件内容:
        full_text = msg.as_string()
        return full_text

    def send_email(self):
        '''发送邮件'''
        # qq邮箱必须加上SSL
        if self.smtp_server == 'smtp.qq.com':
            smtp = smtplib.SMTP_SSL(self.smtp_server)
        else:
            smtp = smtplib.SMTP(self.smtp_server)
        # 登录服务器:.login(user=email_address,password=第三方授权码)
        smtp.login(self.from_user, self.password)
        logger.info('--------邮件发送中--------')
        try:
            logger.info('--------邮件发送成功--------')
            smtp.sendmail(self.from_user, self.user_list, self._part())
        except Exception as e:
            logger.error('发送邮件出错,错误信息为:{0}'.format(e))
        else:
            smtp.close()    # 关闭连接

if __name__ == '__main__':
    from base.base_path import *
    part_text = '附件为自动化测试报告,框架使用了pytest+allure'
    attachment_list = [report_path]
    password = ''
    user_list = ['']
    HandleSendEmail(part_text, attachment_list, password, user_list).send_email()

2.2.3 handle_logger.py

import sys
import logging
from time import strftime
from base.base_path import *

class Logger:

    def __init__(self):
        # 日志格式
    custom_format = '%(asctime)s %(filename)s [line:%(lineno)d] %(levelname)s: %(message)s'
        # 日期格式
        date_format = '%a, %d %b %Y %H:%M:%S'

        self._logger = logging.getLogger()  # 实例化
        self.filename = '{0}{1}.log'.format(log_path, strftime("%Y-%m-%d")) # 日志文件名
        self.formatter = logging.Formatter(fmt=custom_format, datefmt=date_format)
        self._logger.addHandler(self._get_file_handler(self.filename))
        self._logger.addHandler(self._get_console_handler())
        self._logger.setLevel(logging.INFO)  # 默认等级

    def _get_file_handler(self, filename):
        '''输出到日志文件'''
        filehandler = logging.FileHandler(filename, encoding="utf-8")
        filehandler.setFormatter(self.formatter)
        return filehandler

    def _get_console_handler(self):
        '''输出到控制台'''
        console_handler = logging.StreamHandler(sys.stdout)
        console_handler.setFormatter(self.formatter)
        return console_handler

    @property
    def logger(self):
        return self._logger

'''
日志级别:
critical    严重错误,会导致程序退出
error        可控范围内的错误
warning        警告信息
info        提示信息
debug        调试程序时详细输出的记录
'''
# 实例
logger = Logger().logger


if __name__ == '__main__':
    import datetime
    logger.info(u"{}:开始XXX操作".format(datetime.datetime.now()))

2.2.4 handle_config.py

import configparser

# 配置文件类
class HandleConfig:
    def operation_config(self, conf_file, section, option):
        cf = configparser.ConfigParser()    # 实例化
        cf.read(conf_file)
        value = cf.get(section, option)    # 定位
        return value


handle_config = HandleConfig()
if __name__ == '__main__':
    from base.base_path import *
    base_url = handle_config.operation_config(conf_path, 'BASEURL', 'base_url')
    print(base_url)

2.2.5 handle_allure.py

import subprocess
from base.base_path import *

class HandleAllure(object):

    def execute_command(self):
        subprocess.call(allure_command, shell=True)

handle_allure = HandleAllure()

2.3datos de prueba

El archivo de caso de prueba de Excel debe terminar en .xlsx y la estructura del caso de uso es la siguiente:

imagen

configuración 2.4

Coloque el archivo de configuración al final de .conf

2.5 casos de prueba
conftest.py

Función de dispositivo, operaciones previas y posteriores del caso de uso
Construcción de datos de prueba
Otras operaciones avanzadas
Tenga en cuenta que la contraseña y la lista de usuarios en el correo electrónico deben reemplazarse con el correo electrónico y la contraseña de servicio
test_wanAndroid.py script de caso de prueba de su propia prueba

Parametrización: pytest.mark.parametrize('case',[{},{}])
Asociación de interfaz:
configure los parámetros asociados como variables globales.
Utilice variables globales para reemplazar los parámetros antes de que se ejecute el caso de uso.
Utilice el parámetro is_run para indicar el caso de uso parametrizado. , y sáquelo y asígnelo a las
cookies variables globales:
procese las cookies de la misma manera que la asociación de interfaz. Recoja casos de uso
. Ejecute casos de uso . Afirme. Construya un informe de prueba . Envíe un correo electrónico . 2.5. 1 conftest.py





import pytest
from base.base_path import *
from utils.handle_logger import logger
from utils.handle_allure import handle_allure
from utils.handle_sendEmail import HandleSendEmail

'''
1. 构造测试数据??
2. fixture 替代 setup,teardown
3. 配置 pytest
'''

def pytest_collection_modifyitems(items):
    """
    测试用例收集完成时,将收集到的item的name和nodeid的中文显示在控制台上
    """
    for item in items:
        item.name = item.name.encode("utf-8").decode("unicode_escape")
        item._nodeid = item.nodeid.encode("utf-8").decode("unicode_escape")
        # print(item.nodeid)

@pytest.fixture(scope='session', autouse=True)
def send_email():
    logger.info('-----session级,执行wanAndroid测试用例-----')
    yield
    logger.info('-----session级,wanAndroid用例执行结束,发送邮件:-----')
    """执行alllure命令 """
    handle_allure.execute_command()
    # 发邮件
    part_text = '附件为自动化测试报告,框架使用了pytest+allure'
    attachment_list = [report_path]
    password = ''
    user_list = ['']
    HandleSendEmail(part_text, attachment_list, password, user_list).send_email()

2.5.2 test_wanAndroid.py

import json
import pytest
import allure
from base.base_requests import BaseRequests
from utils.handle_logger import logger
from utils.handle_excel import HandleExcel
from utils.param_replace import pr
from utils.handle_cookies import get_cookies

handle_excel = HandleExcel()
get_excel_data = HandleExcel().get_excel_data()
ID = ''
COOKIES = {}
PAGE = ''

class TestWanAndroid:

    @pytest.mark.parametrize('case', get_excel_data)
    def test_wanAndroid(self, case):
        global ID
        global COOKIES
        # 参数替换
        case['url'] = pr.relevant_parameter(case['url'], '${collect_id}', str(ID))

        if case['is_run'].lower() == 'yes':
            logger.info('------执行用例的id为:{0},用例标题为:{1}------'.format(case['case_id'], case['title']))
            res = BaseRequests(case, cookies=COOKIES).get_response()
            res_json = res.json()

            # 获取登录后的cookies
            if case['case_id'] == 3:
                COOKIES = get_cookies.get_cookies(res)

            if case['is_depend']:
                try:
                    ID = res_json['data']['id']
                    # 将使用的参数化后的数据写入excel
                    handle_excel.rewrite_value('id={}'.format(ID), case['case_id'], 'depend_param')
                except Exception as e:
                    logger.error(f'获取id失败,错误信息为{e}')
                    ID = 0

            # 制作 allure 报告
            allure.dynamic.title(case['title'])
            allure.dynamic.description('<font color="red">请求URL:</font>{}<br />'
                                       '<font color="red">期望值:</font>{}'.format(case['url'], case['excepted']))
            allure.dynamic.feature(case['module'])
            allure.dynamic.story(case['method'])

            result=''
            try:
                assert eval(case['excepted'])['errorCode'] == res_json['errorCode']
                result = 'pass'
            except AssertionError as e:
                logger.error('Assert Error:{0}'.format(e))
                result = 'fail'
                raise e
            finally:
                # 将实际结果格式化写入excel
                handle_excel.rewrite_value(json.dumps(res_json, ensure_ascii=False, indent=2, sort_keys=True), case['case_id'], 'actual')
                # 将用例执行结果写入excel
                handle_excel.rewrite_value(result, case['case_id'], 'test_result')


    def test_get_articleList(self):
        '''翻页,将page参数化'''
        global PAGE
        pass


    def test_mock_demo(self):
        '''使用mock服务模拟服务器响应'''
        pass


if __name__ == '__main__':
    pytest.main(['-q', 'test_wanAndroid.py'])

2.6 testReport
almacena informes de prueba html, instale el complemento pip install pytest-html

Almacene el informe de prueba de Allure, instale el complemento pip install allure-pytest

2.7 registros
almacena archivos de registro

2.8 Otros archivos
archivo principal en ejecución run.py

pytest.ini configura el comportamiento predeterminado de pytest, las reglas de ejecución, etc.

requisitos.txt depende del entorno

Generar automáticamente
la instalación de congelación de pip pip -r install requisitos.txt

3. Resumen

  1. Allure tiene muchas operaciones interesantes e incluso controla el comportamiento de ejecución de los casos de uso. Si estás interesado, puedes ampliarlo. También puedes leer el blog anterior.

  2. La dificultad para implementar el marco radica en las dependencias de la interfaz.

  3. La automatización de la interfaz debe evitar dependencias complejas de la interfaz, que solo provocarán pruebas incontrolables.

  4. Tenga en cuenta que las operaciones frecuentes en Excel consumirán rendimiento

  5. Si estás interesado, puedes integrar este framework en Jenkins.

  6. Todas las interfaces de demostración de este artículo se utilizan en este sitio. Gracias al autor por proporcionar interfaces gratuitas
    https://www.wanandroid.com/

  7. Dirección de git del proyecto:...(git está encriptado y se agregará más adelante))

Finalmente me gustaría agradecer a todos los que leyeron atentamente mi artículo, la reciprocidad siempre es necesaria, aunque no es algo muy valioso, si puedes usarlo, puedes tomarlo directamente:

Insertar descripción de la imagen aquí

Esta información debería ser el almacén de preparación más completo y completo para los amigos [de pruebas de software]. Este almacén también ha acompañado a decenas de miles de ingenieros de pruebas en el viaje más difícil. ¡Espero que también pueda ayudarlo a usted!   

Supongo que te gusta

Origin blog.csdn.net/YLF123456789000/article/details/133097010
Recomendado
Clasificación