• python+requests+pytest+allure自动化框架


    1.核心库

    requests request请求

    openpyxl excel文件操作

    loggin 日志

    smtplib 发送邮件

    configparser

    unittest.mock mock服务

    2.目录结构

    base

    utils

    testDatas

    conf

    testCases

    testReport

    logs

    其他

    图片

    2.1base

    base_path.py 存放绝对路径,dos命令或Jenkins执行时,防止报错

    base_requests.py 封装requests,根据method选择不同的方法执行脚本,同时处理请求异常

    2.1.1 base_path.py

    1. import os
    2. # 项目根路径
    3. _root_path = os.path.split(os.path.split(os.path.realpath(__file__))[0])[0]
    4. # 报告路径
    5. report_path = os.path.join(_root_path, 'testReport', 'report.html')
    6. # 日志路径
    7. log_path = os.path.join(_root_path, 'logs/')
    8. # 配置文件路径
    9. conf_path = os.path.join(_root_path, 'conf', 'auto_test.conf')
    10. # 测试数据路径
    11. testdatas_path = os.path.join(_root_path, 'testDatas')
    12. # allure 相关配置
    13. _result_path = os.path.join(_root_path, 'testReport', 'result')
    14. _allure_html_path = os.path.join(_root_path, 'testReport', 'allure_html')
    15. allure_command = 'allure generate {} -o {} --clean'.format(_result_path, _allure_html_path)

    2.1.2 base_requests.py

    1. import json
    2. import allure
    3. import urllib3
    4. import requests
    5. import warnings
    6. from bs4 import BeautifulSoup
    7. from base.base_path import *
    8. from requests.adapters import HTTPAdapter
    9. from utils.handle_logger import logger
    10. from utils.handle_config import handle_config as hc
    11. class BaseRequests:
    12. def __init__(self, case, proxies=None, headers=None, cookies=None, timeout=15, max_retries=3):
    13. '''
    14. :param case: 测试用例
    15. :param proxies: The result is displayed in fiddler:
    16. {"http": "http://127.0.0.1:8888", "https": "https://127.0.0.1:8888"}
    17. :param headers: 请求头
    18. :param cookies: cookies
    19. :param timeout: 请求默认超时时间15s
    20. :param max_retries: 请求超时后默认重试3次
    21. '''
    22. self.case = case
    23. self.proxies = proxies
    24. self.headers = headers
    25. self.cookies = cookies
    26. self.timeout = timeout
    27. self.max_retries = max_retries
    28. self.base_url = hc.operation_config(conf_path, 'BASEURL', 'base_url')
    29. def get_response(self):
    30. '''获取请求结果'''
    31. response = self._run_main()
    32. return response
    33. def _run_main(self):
    34. '''发送请求'''
    35. method = self.case['method']
    36. url = self.base_url + self.case['url']
    37. if self.case['parameter']:
    38. data = eval(self.case['parameter'])
    39. else:
    40. data = None
    41. s = requests.session()
    42. s.mount('http://', HTTPAdapter(max_retries=self.max_retries))
    43. s.mount('https://', HTTPAdapter(max_retries=self.max_retries))
    44. urllib3.disable_warnings() # 忽略浏览器认证(https认证)警告
    45. warnings.simplefilter('ignore', ResourceWarning) # 忽略 ResourceWarning警告
    46. res=''
    47. if method.upper() == 'POST':
    48. try:
    49. res = s.request(method='post', url=url, data=data, verify=False, proxies=self.proxies, headers=self.headers, cookies=self.cookies, timeout=self.timeout)
    50. except Exception as e:
    51. logger.error('POST请求出错,错误信息为:{0}'.format(e))
    52. elif method.upper() == 'GET':
    53. try:
    54. res = s.request(method='get', url=url, params=data, verify=False,proxies=self.proxies, headers=self.headers, cookies=self.cookies, timeout=self.timeout)
    55. except Exception as e:
    56. logger.error('GET请求出错,错误信息为:{0}'.format(e))
    57. else:
    58. raise ValueError('method方法为get和post')
    59. logger.info(f'请求方法:{method},请求路径:{url}, 请求参数:{data}, 请求头:{self.headers}, cookies:{self.cookies}')
    60. # with allure.step('接口请求信息:'):
    61. # allure.attach(f'请求方法:{method},请求路径:{url}, 请求参数:{data}, 请求头:{headers}')
    62. # 拓展:是否需要做全量契约验证?响应结果是不同类型时,如何处理响应?
    63. return res
    64. if __name__ == '__main__':
    65. # case = {'method': 'get', 'url': '/article/top/json', 'parameter': ''}
    66. case = {'method': 'post', 'url': '/user/login', 'parameter': '{"username": "xbc", "password": "123456"}'}
    67. response = BaseRequests(case).get_response()
    68. print(response.json())

    2.2 utils

    (只取核心部分)

    handle_excel.py

    excel的操作,框架要求,最终读取的数据需要保存列表嵌套字典的格式[{},{}]
    其他操作
    handle_sendEmail.py

    python发送邮件使用smtp协议,接收邮件使用pop3
    需要开启pop3服务功能,这里的password为授权码,启用服务自行百度
    handle_logger.py 日志处理

    handle_config.py
    配置文件处理,这里只将域名可配置化,切换环境时改域名即可

    handle_allure.py
    allure生成的报告需要调用命令行再打开,这里直接封装命令

    handle_cookies.py(略)
    在git中补充,处理cookiesJar对象

    handle_mock.py(略)
    在git中补充,框架未使用到,但是也封装成了方法

    param_replace(略)

    将常用的参数化操作封装成类

    2.2.1 handle_excel.py

    1. import openpyxl
    2. from base.base_path import *
    3. class HandleExcel:
    4. def __init__(self, file_name=None, sheet_name=None):
    5. '''
    6. 没有传路径时,默认使用 wanadriod接口测试用例.xlsx 文件
    7. :param file_name: 用例文件
    8. :param sheet_name: 表单名
    9. '''
    10. if file_name:
    11. self.file_path = os.path.join(testdatas_path, file_name)
    12. self.sheet_name = sheet_name
    13. else:
    14. self.file_path = os.path.join(testdatas_path, 'wanadriod接口测试用例.xlsx')
    15. self.sheet_name = 'case'
    16. # 创建工作簿,定位表单
    17. self.wb = openpyxl.load_workbook(self.file_path)
    18. self.sheet = self.wb[self.sheet_name]
    19. # 列总数,行总数
    20. self.ncols = self.sheet.max_column
    21. self.nrows = self.sheet.max_row
    22. def cell_value(self, row=1, column=1):
    23. '''获取表中数据,默认取出第一行第一列的值'''
    24. return self.sheet.cell(row, column).value
    25. def _get_title(self):
    26. '''私有函数, 返回表头列表'''
    27. title = []
    28. for column in range(1, self.ncols+1):
    29. title.append(self.cell_value(1, column))
    30. return title
    31. def get_excel_data(self):
    32. '''
    33. :return: 返回字典套列表的方式 [{title_url:value1, title_method:value1}, {title_url:value2, title_method:value2}...]
    34. '''
    35. finally_data = []
    36. for row in range(2, self.nrows+1):
    37. result_dict = {}
    38. for column in range(1, self.ncols+1):
    39. result_dict[self._get_title()[column-1]] = self.cell_value(row, column)
    40. finally_data.append(result_dict)
    41. return finally_data
    42. def get_pytestParametrizeData(self):
    43. '''
    44. 选用这种参数方式,需要使用数据格式 列表套列表 @pytest.mark.parametrize('', [[], []]), 如 @pytest.mark.parametrize(*get_pytestParametrizeData)
    45. 将 finally_data 中的 title 取出,以字符串形式保存,每个title用逗号(,)隔开
    46. 将 finally_data 中的 value 取出,每行数据保存在一个列表,再集合在一个大列表内
    47. :return: title, data
    48. '''
    49. finally_data = self.get_excel_data()
    50. data = []
    51. title = ''
    52. for i in finally_data:
    53. value_list = []
    54. key_list = []
    55. for key, value in i.items():
    56. value_list.append(value)
    57. key_list.append(key)
    58. title = ','.join(key_list)
    59. data.append(value_list)
    60. return title, data
    61. def rewrite_value(self, new_value, case_id, title):
    62. '''写入excel,存储使用过的数据(参数化后的数据)'''
    63. row = self.get_row(case_id)
    64. column = self.get_column(title)
    65. self.sheet.cell(row, column).value = new_value
    66. self.wb.save(self.file_path)
    67. def get_row(self, case_id):
    68. '''通过执行的 case_id 获取当前的行号'''
    69. for row in range(1, self.nrows+1):
    70. if self.cell_value(row, 1) == case_id:
    71. return int(row)
    72. def get_column(self, title):
    73. '''通过表头给定字段,获取表头所在列'''
    74. for column in range(1, self.ncols+1):
    75. if self.cell_value(1, column) == title:
    76. return int(column)
    77. if __name__ == '__main__':
    78. r = HandleExcel()
    79. print(r.get_excel_data())

    2.2.2 handle_sendEmail.py

    1. import smtplib
    2. from utils.handle_logger import logger
    3. from email.mime.text import MIMEText # 专门发送正文邮件
    4. from email.mime.multipart import MIMEMultipart # 发送正文、附件等
    5. from email.mime.application import MIMEApplication # 发送附件
    6. class HandleSendEmail:
    7. def __init__(self, part_text, attachment_list, password, user_list, subject='interface_autoTestReport', smtp_server='smtp.163.com', from_user='hu_chunpu@163.com', filename='unit_test_report.html'):
    8. '''
    9. :param part_text: 正文
    10. :param attachment_list: 附件列表
    11. :param password: 邮箱服务器第三方密码
    12. :param user_list: 收件人列表
    13. :param subject: 主题
    14. :param smtp_server: 邮箱服务器
    15. :param from_user: 发件人
    16. :param filename: 附件名称
    17. '''
    18. self.subject = subject
    19. self.attachment_list = attachment_list
    20. self.password = password
    21. self.user_list = ';'.join(user_list) # 多个收件人
    22. self.part_text = part_text
    23. self.smtp_server = smtp_server
    24. self.from_user = from_user
    25. self.filename = filename
    26. def _part(self):
    27. '''构建邮件内容'''
    28. # 1) 构造邮件集合体:
    29. msg = MIMEMultipart()
    30. msg['Subject'] = self.subject
    31. msg['From'] = self.from_user
    32. msg['To'] = self.user_list
    33. # 2) 构造邮件正文:
    34. text = MIMEText(self.part_text)
    35. msg.attach(text) # 把正文加到邮件体里面
    36. # 3) 构造邮件附件:
    37. for item in self.attachment_list:
    38. with open(item, 'rb+') as file:
    39. attachment = MIMEApplication(file.read())
    40. # 给附件命名:
    41. attachment.add_header('Content-Disposition', 'attachment', filename=item)
    42. msg.attach(attachment)
    43. # 4) 得到完整的邮件内容:
    44. full_text = msg.as_string()
    45. return full_text
    46. def send_email(self):
    47. '''发送邮件'''
    48. # qq邮箱必须加上SSL
    49. if self.smtp_server == 'smtp.qq.com':
    50. smtp = smtplib.SMTP_SSL(self.smtp_server)
    51. else:
    52. smtp = smtplib.SMTP(self.smtp_server)
    53. # 登录服务器:.login(user=email_address,password=第三方授权码)
    54. smtp.login(self.from_user, self.password)
    55. logger.info('--------邮件发送中--------')
    56. try:
    57. logger.info('--------邮件发送成功--------')
    58. smtp.sendmail(self.from_user, self.user_list, self._part())
    59. except Exception as e:
    60. logger.error('发送邮件出错,错误信息为:{0}'.format(e))
    61. else:
    62. smtp.close() # 关闭连接
    63. if __name__ == '__main__':
    64. from base.base_path import *
    65. part_text = '附件为自动化测试报告,框架使用了pytest+allure'
    66. attachment_list = [report_path]
    67. password = ''
    68. user_list = ['']
    69. HandleSendEmail(part_text, attachment_list, password, user_list).send_email()

    2.2.3 handle_logger.py

    1. import sys
    2. import logging
    3. from time import strftime
    4. from base.base_path import *
    5. class Logger:
    6. def __init__(self):
    7. # 日志格式
    8. custom_format = '%(asctime)s %(filename)s [line:%(lineno)d] %(levelname)s: %(message)s'
    9. # 日期格式
    10. date_format = '%a, %d %b %Y %H:%M:%S'
    11. self._logger = logging.getLogger() # 实例化
    12. self.filename = '{0}{1}.log'.format(log_path, strftime("%Y-%m-%d")) # 日志文件名
    13. self.formatter = logging.Formatter(fmt=custom_format, datefmt=date_format)
    14. self._logger.addHandler(self._get_file_handler(self.filename))
    15. self._logger.addHandler(self._get_console_handler())
    16. self._logger.setLevel(logging.INFO) # 默认等级
    17. def _get_file_handler(self, filename):
    18. '''输出到日志文件'''
    19. filehandler = logging.FileHandler(filename, encoding="utf-8")
    20. filehandler.setFormatter(self.formatter)
    21. return filehandler
    22. def _get_console_handler(self):
    23. '''输出到控制台'''
    24. console_handler = logging.StreamHandler(sys.stdout)
    25. console_handler.setFormatter(self.formatter)
    26. return console_handler
    27. @property
    28. def logger(self):
    29. return self._logger
    30. '''
    31. 日志级别:
    32. critical 严重错误,会导致程序退出
    33. error 可控范围内的错误
    34. warning 警告信息
    35. info 提示信息
    36. debug 调试程序时详细输出的记录
    37. '''
    38. # 实例
    39. logger = Logger().logger
    40. if __name__ == '__main__':
    41. import datetime
    42. logger.info(u"{}:开始XXX操作".format(datetime.datetime.now()))

    2.2.4 handle_config.py

    1. import configparser
    2. # 配置文件类
    3. class HandleConfig:
    4. def operation_config(self, conf_file, section, option):
    5. cf = configparser.ConfigParser() # 实例化
    6. cf.read(conf_file)
    7. value = cf.get(section, option) # 定位
    8. return value
    9. handle_config = HandleConfig()
    10. if __name__ == '__main__':
    11. from base.base_path import *
    12. base_url = handle_config.operation_config(conf_path, 'BASEURL', 'base_url')
    13. print(base_url)

    2.2.5 handle_allure.py

    1. import subprocess
    2. from base.base_path import *
    3. class HandleAllure(object):
    4. def execute_command(self):
    5. subprocess.call(allure_command, shell=True)
    6. handle_allure = HandleAllure()

    2.3testDatas

    excel测试用例文件,必须是.xlsx结尾,用例结构如下:

    图片

    2.4conf

    放置配置文件 .conf结尾

    2.5 testCases
    conftest.py

    fixture功能,用例前置后置操作
    构造测试数据
    其他高级操作
    注意邮件中的password和user_list需要换成自己测试的邮箱及服务密码
    test_wanAndroid.py 测试用例脚本

    参数化: pytest.mark.parametrize(‘case’,[{},{}])
    接口关联:
    将关联的参数配置成全局变量
    在用例执行前使用全局变量替换参数
    使用 is_run 参数指明有参数化的用例,并取出,再赋值给全局变量
    cookies:
    和接口关联的处理方式一样处理cookies
    步骤
    收集用例
    执行用例
    断言
    构造测试报告
    发送邮件
    2.5.1 conftest.py

    1. import pytest
    2. from base.base_path import *
    3. from utils.handle_logger import logger
    4. from utils.handle_allure import handle_allure
    5. from utils.handle_sendEmail import HandleSendEmail
    6. '''
    7. 1. 构造测试数据??
    8. 2. fixture 替代 setup,teardown
    9. 3. 配置 pytest
    10. '''
    11. def pytest_collection_modifyitems(items):
    12. """
    13. 测试用例收集完成时,将收集到的item的name和nodeid的中文显示在控制台上
    14. """
    15. for item in items:
    16. item.name = item.name.encode("utf-8").decode("unicode_escape")
    17. item._nodeid = item.nodeid.encode("utf-8").decode("unicode_escape")
    18. # print(item.nodeid)
    19. @pytest.fixture(scope='session', autouse=True)
    20. def send_email():
    21. logger.info('-----session级,执行wanAndroid测试用例-----')
    22. yield
    23. logger.info('-----session级,wanAndroid用例执行结束,发送邮件:-----')
    24. """执行alllure命令 """
    25. handle_allure.execute_command()
    26. # 发邮件
    27. part_text = '附件为自动化测试报告,框架使用了pytest+allure'
    28. attachment_list = [report_path]
    29. password = ''
    30. user_list = ['']
    31. HandleSendEmail(part_text, attachment_list, password, user_list).send_email()

    2.5.2 test_wanAndroid.py

    1. import json
    2. import pytest
    3. import allure
    4. from base.base_requests import BaseRequests
    5. from utils.handle_logger import logger
    6. from utils.handle_excel import HandleExcel
    7. from utils.param_replace import pr
    8. from utils.handle_cookies import get_cookies
    9. handle_excel = HandleExcel()
    10. get_excel_data = HandleExcel().get_excel_data()
    11. ID = ''
    12. COOKIES = {}
    13. PAGE = ''
    14. class TestWanAndroid:
    15. @pytest.mark.parametrize('case', get_excel_data)
    16. def test_wanAndroid(self, case):
    17. global ID
    18. global COOKIES
    19. # 参数替换
    20. case['url'] = pr.relevant_parameter(case['url'], '${collect_id}', str(ID))
    21. if case['is_run'].lower() == 'yes':
    22. logger.info('------执行用例的id为:{0},用例标题为:{1}------'.format(case['case_id'], case['title']))
    23. res = BaseRequests(case, cookies=COOKIES).get_response()
    24. res_json = res.json()
    25. # 获取登录后的cookies
    26. if case['case_id'] == 3:
    27. COOKIES = get_cookies.get_cookies(res)
    28. if case['is_depend']:
    29. try:
    30. ID = res_json['data']['id']
    31. # 将使用的参数化后的数据写入excel
    32. handle_excel.rewrite_value('id={}'.format(ID), case['case_id'], 'depend_param')
    33. except Exception as e:
    34. logger.error(f'获取id失败,错误信息为{e}')
    35. ID = 0
    36. # 制作 allure 报告
    37. allure.dynamic.title(case['title'])
    38. allure.dynamic.description('请求URL:{}
      '
    39. '期望值:{}'.format(case['url'], case['excepted']))
    40. allure.dynamic.feature(case['module'])
    41. allure.dynamic.story(case['method'])
    42. result=''
    43. try:
    44. assert eval(case['excepted'])['errorCode'] == res_json['errorCode']
    45. result = 'pass'
    46. except AssertionError as e:
    47. logger.error('Assert Error:{0}'.format(e))
    48. result = 'fail'
    49. raise e
    50. finally:
    51. # 将实际结果格式化写入excel
    52. handle_excel.rewrite_value(json.dumps(res_json, ensure_ascii=False, indent=2, sort_keys=True), case['case_id'], 'actual')
    53. # 将用例执行结果写入excel
    54. handle_excel.rewrite_value(result, case['case_id'], 'test_result')
    55. def test_get_articleList(self):
    56. '''翻页,将page参数化'''
    57. global PAGE
    58. pass
    59. def test_mock_demo(self):
    60. '''使用mock服务模拟服务器响应'''
    61. pass
    62. if __name__ == '__main__':
    63. pytest.main(['-q', 'test_wanAndroid.py'])

    2.6 testReport
    存放html测试报告,安装插件pip install pytest-html

    存放allure测试报告,插件安装pip install allure-pytest

    2.7 logs
    存放日志文件

    2.8 其他文件
    run.py 主运行文件

    pytest.ini 配置pytest的默认行为,运行规则等

    requirements.txt 依赖环境

    自动生成 pip freeze
    安装 pip -r install requirements.txt

    3.总结

    1. allure有很多有趣的操作,甚至控制用例执行行为,有兴趣可以拓展,也可以看下之前的博客

    2. 实现框架的难点在接口依赖

    3. 接口自动化应避免复杂的接口依赖,复杂的依赖只会造成测试的不可控性

    4. 注意频繁的操作excel会消耗性能

    5. 有兴趣可以将本框架集合在Jenkins中

    6. 本文的demo接口均采用至本站,感谢作者提供的免费接口
      https://www.wanandroid.com/

    7. 项目git地址:…(git加密了,后续补上))

    最后感谢每一个认真阅读我文章的人,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:

    在这里插入图片描述

    这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你!   

  • 相关阅读:
    企业为什么要做等保?不做等保有什么后果?
    最简单的修改linux系统上Docker的镜像源
    java计算机毕业设计普通中学教职工信息管理系统源码+系统+数据库+lw文档+mybatis+运行部署
    C# 程序兼容同一个dll的不同版本
    WSL安装和嵌入式Linux的树莓派环境设置和交叉编译
    【Python】基础练习题_ 函数和代码复用
    C++对象和类概述
    【DevOps】Git 核心操作命令——掌握了就算入门了Git
    OPENCV简单阈值中的参数详解
    计算机智能专题-遗传算法(1不带约束的)
  • 原文地址:https://blog.csdn.net/YLF123456789000/article/details/133097010