• python excel接口自动化测试框架


    前言

    前些天写了pytest+yaml+allure接口自动化测试框架这篇文章。

    今天采用Excel继续写一个接口自动化测试框架。

    设计流程图

    这张图是我的excel接口测试框架的一些设计思路。

    首先读取excel文件,得到测试信息,然后通过封装的requests方法,用unittest进行测试。

    其中,接口关联的参数通过正则进行查找和替换,为此我专门开辟了一个全局变量池,用于管理各种各样的变量。

    最后通过HTMLrunner生成测试报告。如果执行失败,发送测试报告结果邮件。

    Excel和结果预览

    这个时excel的测试测试用例组织结构图。

    这个是运行之后生成的HTML测试报告。

    这个时运行之后生成的excel报告。可以看到我故意在预期正则中设置了错误的值,然后用例失败的同时也把失败的预期值标记出来了。

    img
    测试失败之后收到的邮件
    在这里插入图片描述

    好了上面就是一些简单的介绍,我们开始进入正题把。

    框架结构

    首先,要开发这样一个excel接口自动化测试项目必须有一个设计清晰的思路,这样我们在开发框架的过程中才会明白自己要干什么。

    目录/文件说明是否为python包
    common公共类
    core核心类,封装requests
    data测试使用的excel文件存放目录
    logs日志目录
    tests测试用例目录
    utils工具类,如:日志
    config.py配置文件
    run.py执行文件

    Excel相关

    用例设计

    本次依然采用的是智学网登录接口。使用了智学网中的登录接口和登录验证接口,这两个接口之间有依赖的参数。

    配置文件

    在项目的根目录创建config.py,把你能想到的配置信息,全部丢在这个文件中进行统一的管理。

    1. #!/usr/bin/env python3
    2. # coding=utf-8
    3. import os
    4. class CF:
    5. """配置文件"""
    6. # 项目目录
    7. BASE_DIR = os.path.abspath(os.path.dirname(__file__))
    8. # Excel首行配置
    9. NUMBER = 0
    10. NAME = 1
    11. METHOD = 2
    12. URL = 3
    13. ROUTE = 4
    14. HEADERS = 5
    15. PARAMETER = 6 # 参数
    16. EXPECTED_CODE = 7 # 预期响应码
    17. EXPECTED_REGULAR = 8 # 预期正则
    18. EXPECTED_VALUE = 9 # 预期结果值
    19. SPEND_TIME = 10 # 响应时间
    20. TEST_RESULTS = 11 # 测试结果
    21. EXTRACT_VARIABLE = 12 # 提取变量
    22. RESPONSE_TEXT = 13 # 响应文本
    23. # 字体大小
    24. FONT_SET = "微软雅黑"
    25. FONT_SIZE = 16
    26. # 颜色配置
    27. COLOR_PASSED = "90EE90"
    28. COLOR_FAILED = "FA8072"
    29. # 邮箱配置
    30. EMAIL_INFO = {
    31. 'username': '1084502012@qq.com',
    32. 'password': 2,
    33. 'smtp_host': 'smtp.qq.com',
    34. 'smtp_port': 465
    35. }
    36. # 收件人
    37. ADDRESSEE = ['1084502012@qq.com']
    38. if __name__ == '__main__':
    39. print(CF.EXPECTED_CODE)
    读取/写入excel#

    ​ 在common目录中新建excelset.py文件,在这个文件中我们要实现,读取excel中的用例,写入测试结果并绘制相应的颜色,写入测试耗费时长。

    1. #!/usr/bin/env python
    2. # coding=utf-8
    3. import shutil
    4. import openpyxl
    5. from config import CF
    6. from openpyxl.styles import Font
    7. from openpyxl.styles import PatternFill
    8. from common.variables import VariablePool
    9. class ExcelSet:
    10. """Excel配置"""
    11. def __init__(self):
    12. shutil.copyfile(VariablePool.get('excel_input'), VariablePool.get('excel_output'))
    13. self.path = VariablePool.get('excel_output')
    14. self.wb = openpyxl.load_workbook(self.path)
    15. self.table = self.wb.active
    16. def get_cases(self, min_row=2):
    17. """获取用例"""
    18. all_cases = []
    19. for row in self.table.iter_rows(min_row=min_row):
    20. all_cases.append((self.table.cell(min_row, CF.NAME + 1).value,
    21. min_row, [cell.value for cell in row]))
    22. min_row += 1
    23. return all_cases
    24. def write_color(self, row_n, col_n, color=CF.COLOR_FAILED):
    25. """写入颜色"""
    26. cell = self.table.cell(row_n, col_n + 1)
    27. fill = PatternFill("solid", fgColor=color)
    28. cell.fill = fill
    29. def write_results(self, row_n, col_n, value, color=True):
    30. """写入结果"""
    31. cell = self.table.cell(row_n, col_n + 1)
    32. cell.value = value
    33. font = Font(name=CF.FONT_SET, size=CF.FONT_SIZE)
    34. cell.font = font
    35. if color:
    36. if value.lower() in ("fail", 'failed'):
    37. fill = PatternFill("solid", fgColor=CF.COLOR_FAILED)
    38. cell.fill = fill
    39. elif value.lower() in ("pass", "ok"):
    40. fill = PatternFill("solid", fgColor=CF.COLOR_PASSED)
    41. cell.fill = fill
    42. self.wb.save(self.path)
    43. excel_set = ExcelSet()
    44. if __name__ == '__main__':
    45. print(excel_set.get_cases())

    日志封装

    logger.py

    在一个项目中日志是必不可少的东西,可以第一时间反馈问题。

    1. #!/usr/bin/env python3
    2. # coding=utf-8
    3. import os
    4. import logging
    5. from config import CF
    6. from datetime import datetime
    7. class Logger:
    8. def __init__(self):
    9. self.logger = logging.getLogger()
    10. if not self.logger.handlers:
    11. self.logger.setLevel(logging.DEBUG)
    12. # 创建一个handler,用于写入日志文件
    13. fh = logging.FileHandler(self.log_path, encoding='utf-8')
    14. fh.setLevel(logging.DEBUG)
    15. # 创建一个handler,用于输出到控制台
    16. ch = logging.StreamHandler()
    17. ch.setLevel(logging.INFO)
    18. # 定义handler的输出格式
    19. formatter = logging.Formatter(self.fmt)
    20. fh.setFormatter(formatter)
    21. ch.setFormatter(formatter)
    22. # 给logger添加handler
    23. self.logger.addHandler(fh)
    24. self.logger.addHandler(ch)
    25. @property
    26. def log_path(self):
    27. logs_path = os.path.join(CF.BASE_DIR, 'logs')
    28. if not os.path.exists(logs_path):
    29. os.makedirs(logs_path)
    30. now_month = datetime.now().strftime("%Y%m")
    31. return os.path.join(logs_path, '{}.log'.format(now_month))
    32. @property
    33. def fmt(self):
    34. return '%(levelname)s %(asctime)s %(filename)s:%(lineno)d %(message)s'
    35. log = Logger().logger
    36. if __name__ == '__main__':
    37. log.info("你好")

    正则操作

    regular.py

    在接口关联参数的提取和传参中的起到了决定性的作用。

    1. #!/usr/bin/env python3
    2. # -*- coding:utf-8 -*-
    3. import re
    4. from utils.logger import log
    5. from common.variables import VariablePool
    6. from core.serialize import is_json_str
    7. class Regular:
    8. """正则类"""
    9. def __init__(self):
    10. self.reg = re.compile
    11. def finds(self, string):
    12. return self.reg(r'\{{(.*?)}\}').findall(string)
    13. def subs(self, keys, string):
    14. result = None
    15. log.info("提取变量:{}".format(keys))
    16. for i in keys:
    17. if VariablePool.has(i):
    18. log.info("替换变量:{}".format(i))
    19. comment = self.reg(r"\{{%s}}" % i)
    20. result = comment.sub(VariablePool.get(i), string)
    21. log.info("替换结果:{}".format(result))
    22. return result
    23. def find_res(self, exp, string):
    24. """在结果中查找"""
    25. if is_json_str(string):
    26. return self.reg(r'\"%s":"(.*?)"' % exp).findall(string)[0]
    27. else:
    28. return self.reg(r'%s' % exp).findall(string)[0]
    29. if __name__ == '__main__':
    30. a = "{'data': {'loginName': 18291900215, 'password': '{{dd636482aca022}}', 'code': None, 'description': 'encrypt'}}"
    31. print(Regular().finds(a))

    核心操作

    定义变量池

    variables.py

    全局变量池来了,是不是很简单,但是作用确实很巨大的。

    1. #!/usr/bin/env python3
    2. # -*- coding:utf-8 -*-
    3. class VariablePool:
    4. """全局变量池"""
    5. @staticmethod
    6. def get(name):
    7. """获取变量"""
    8. return getattr(VariablePool, name)
    9. @staticmethod
    10. def set(name, value):
    11. """设置变量"""
    12. setattr(VariablePool, name, value)
    13. @staticmethod
    14. def has(name):
    15. return hasattr(VariablePool, name)
    16. if __name__ == '__main__':
    17. VariablePool.set('name', 'wxhou')
    18. print(VariablePool.get('name'))
    封装requests

    request.py

    最最核心的部分,对于python requests库的二次封装。用以实现接口的请求和返回结果的获取。

    1. #!/usr/bin/env python
    2. # coding=utf-8
    3. import urllib3
    4. import requests
    5. from config import CF
    6. from utils.logger import log
    7. from common.regular import Regular
    8. from common.setResult import replace_param
    9. from core.serialize import deserialization
    10. from requests.exceptions import RequestException
    11. from common.variables import VariablePool
    12. urllib3.disable_warnings()
    13. class HttpRequest:
    14. """二次封装requests方法"""
    15. http_method_names = 'get', 'post', 'put', 'delete', 'patch', 'head', 'options'
    16. def __init__(self):
    17. self.r = requests.session()
    18. self.reg = Regular()
    19. def send_request(self, case, **kwargs):
    20. """发送请求
    21. :param case: 测试用例
    22. :param kwargs: 其他参数
    23. :return: request响应
    24. """
    25. if case[CF.URL]:
    26. VariablePool.set('url', case[CF.URL])
    27. if case[CF.HEADERS]:
    28. VariablePool.set('headers', deserialization(case[CF.HEADERS]))
    29. method = case[CF.METHOD].upper()
    30. url = VariablePool.get('url') + case[CF.ROUTE]
    31. self.r.headers = VariablePool.get('headers')
    32. params = replace_param(case)
    33. if params: kwargs = params
    34. try:
    35. log.info("Request Url: {}".format(url))
    36. log.info("Request Method: {}".format(method))
    37. log.info("Request Data: {}".format(kwargs))
    38. def dispatch(method, *args, **kwargs):
    39. if method in self.http_method_names:
    40. handler = getattr(self.r, method)
    41. return handler(*args, **kwargs)
    42. else:
    43. raise AttributeError('request method is ERROR!')
    44. response = dispatch(method.lower(), url, **kwargs)
    45. log.info(response)
    46. log.info("Response Data: {}".format(response.text))
    47. return response
    48. except RequestException as e:
    49. log.exception(format(e))
    50. except Exception as e:
    51. raise e
    序列化与反序列化

    serialize.py

    1. #!/usr/bin/env python3
    2. # -*- coding:utf-8 -*-
    3. import json
    4. from json.decoder import JSONDecodeError
    5. def deserialization(content: json):
    6. """
    7. 反序列化
    8. json对象 -> python数据类型
    9. """
    10. return json.loads(content)
    11. def serialization(content, ensure_ascii=True):
    12. """
    13. 序列化
    14. python数据类型 -> json对象
    15. """
    16. return json.dumps(content, ensure_ascii=ensure_ascii)
    17. def is_json_str(string):
    18. """判断是否是json格式字符串"""
    19. if isinstance(string, str):
    20. try:
    21. json.loads(string)
    22. return True
    23. except JSONDecodeError:
    24. return False
    25. return False
    26. if __name__ == '__main__':
    27. a = "{'data': {'loginName': 18291900215, 'password': 'dd636482aca022', 'code': None, 'description': 'encrypt'}}"
    28. print(is_json_str(a))
    检查结果

    checkResult.py

    在这个文件中,我们将对测试返回的结果进行预期的验证。

    1. #!/usr/bin/env python3
    2. # -*- coding:utf-8 -*-
    3. import re
    4. from config import CF
    5. from utils.logger import log
    6. from requests import Response
    7. from common.excelset import excel_set
    8. def check_result(r: Response, number, case):
    9. """获取结果"""
    10. results = []
    11. excel_set.write_results(number, CF.SPEND_TIME, r.elapsed.total_seconds(), color=False)
    12. if case[CF.EXPECTED_CODE]:
    13. res = int(case[CF.EXPECTED_CODE]) == r.status_code
    14. results.append(res)
    15. if not res: excel_set.write_color(number, CF.EXPECTED_CODE)
    16. log.info(f"预期响应码:{case[CF.EXPECTED_CODE]},实际响应码:{r.status_code}")
    17. if case[CF.EXPECTED_VALUE]:
    18. res = case[CF.EXPECTED_VALUE] in r.text
    19. results.append(res)
    20. if not res: excel_set.write_color(number, CF.EXPECTED_VALUE)
    21. log.info(f"预期响应值:{case[CF.EXPECTED_VALUE]},实际响应值:{r.text}")
    22. if case[CF.EXPECTED_REGULAR]:
    23. res = r'%s' % case[CF.EXPECTED_REGULAR]
    24. ref = re.findall(res, r.text)
    25. results.append(ref)
    26. if not ref: excel_set.write_color(number, CF.EXPECTED_REGULAR)
    27. log.info(f"预期正则:{res},响应{ref}")
    28. if all(results):
    29. excel_set.write_results(number, CF.TEST_RESULTS, 'Pass')
    30. log.info(f"用例【{case[CF.NAME]}】测试成功!")
    31. else:
    32. excel_set.write_results(number, CF.TEST_RESULTS, 'Failed')
    33. assert all(results), f"用例【{case[CF.NUMBER]}{case[CF.NAME]}】测试失败:{results}"
    设置参数

    setResult.py

    在这个文件中我们实现了接口返回值的提取,实现了接口传递参数的函数。

    1. #!/usr/bin/env python3
    2. # -*- coding:utf-8 -*-
    3. from requests import Response
    4. from utils.logger import log
    5. from common.regular import Regular
    6. from common.excelset import excel_set
    7. from common.variables import VariablePool
    8. from core.serialize import is_json_str, deserialization
    9. from config import CF
    10. reg = Regular()
    11. def get_var_result(r: Response, number, case):
    12. """替换变量"""
    13. if case[CF.EXTRACT_VARIABLE]:
    14. for i in case[CF.EXTRACT_VARIABLE].split(','):
    15. result = reg.find_res(i, r.text)
    16. VariablePool.set(i, result)
    17. log.info(f"提取变量{i}={result}")
    18. if not VariablePool.get(i):
    19. excel_set.write_results(number, CF.EXTRACT_VARIABLE, f"提变量{i}失败")
    20. excel_set.write_results(number, CF.RESPONSE_TEXT,
    21. f"ResponseCode:{r.status_code}\nResponseText:{r.text}")
    22. def replace_param(case):
    23. """传入参数"""
    24. if case[CF.PARAMETER]:
    25. if is_json_str(case[CF.PARAMETER]):
    26. is_extract = reg.finds(case[CF.PARAMETER])
    27. if is_extract:
    28. return deserialization(reg.subs(is_extract, case[CF.PARAMETER]))
    29. return deserialization(case[CF.PARAMETER])

    测试操作

    test_api.py

    我们采用unittest进行测试,在前置条件和后置条件中我们对封装的HttpRequest方法进行了初始化和关闭会话操作。

    使用parameterized库中的expend方法对excel中的用例进行参数化读取执行。

    1. #!/usr/bin/env python
    2. # coding=utf-8
    3. import unittest
    4. from parameterized import parameterized
    5. from common.excelset import excel_set
    6. from core.request import HttpRequest
    7. from common.checkResult import check_result
    8. from common.setResult import get_var_result
    9. class TestApi(unittest.TestCase):
    10. """测试接口"""
    11. @classmethod
    12. def setUpClass(cls) -> None:
    13. cls.req = HttpRequest()
    14. @classmethod
    15. def tearDownClass(cls) -> None:
    16. cls.req.r.close()
    17. @parameterized.expand(excel_set.get_cases())
    18. def test_api(self, name, number, case):
    19. """
    20. 测试excel接口用例
    21. """
    22. r = self.req.send_request(case)
    23. get_var_result(r, number, case)
    24. check_result(r, number, case)
    25. if __name__ == '__main__':
    26. unittest.main(verbosity=2)

    测试报告发送邮件类

    run.py

    1. #!/usr/bin/env python3
    2. # -*- coding:utf-8 -*-
    3. import os
    4. import platform
    5. import argparse
    6. import unittest
    7. from common.variables import VariablePool
    8. from utils.send_mail import send_report_mail
    9. from utils.HTMLTestRunner import HTMLTestRunner
    10. def running(path):
    11. """运行"""
    12. test_case = unittest.defaultTestLoader.discover('tests', 'test*.py')
    13. with open(path, 'wb') as fp:
    14. runner = HTMLTestRunner(stream=fp,
    15. title='Excel接口测试',
    16. description="用例执行情况",
    17. verbosity=2)
    18. result = runner.run(test_case)
    19. if result.failure_count:
    20. send_report_mail(path)
    21. def file_path(arg):
    22. """获取输入的文件路径"""
    23. if 'Windows' in platform.platform():
    24. _dir = os.popen('chdir').read().strip()
    25. else:
    26. _dir = os.popen('pwd').read().strip()
    27. if _dir in arg:
    28. return arg
    29. return os.path.join(_dir, arg)
    30. def main():
    31. """主函数"""
    32. parser = argparse.ArgumentParser(description="运行Excel接口测试")
    33. parser.add_argument('-i', type=str, help='原始文件')
    34. parser.add_argument('-o', type=str, default='report.xlsx', help="输出文件")
    35. parser.add_argument('-html', type=str, default='report.html', help="报告文件")
    36. args = parser.parse_args()
    37. VariablePool.set('excel_input', file_path(args.i))
    38. VariablePool.set('excel_output', file_path(args.o))
    39. VariablePool.set('report_path', file_path(args.html))
    40. running(VariablePool.get('report_path'))
    41. if __name__ == '__main__':
    42. main()

    运行

    值得注意的是,运行测试时要关闭office打开该excel文件。

    最后的文件中我是使用了argparse进行了命令行管理,意味着我们可以通过命令行进行测试而无需关心excel在那个目录下存放着。

    python run.py -i data\usercase.xlsx
    

    输入下面的命令执行一下。

    1. INFO 2020-07-30 22:07:52,713 request.py:40 Request Url: https://www.zhixue.com/weakPwdLogin/?from=web_login
    2. INFO 2020-07-30 22:07:52,714 request.py:41 Request Method: POST
    3. INFO 2020-07-30 22:07:52,715 request.py:42 Request Data: {'data': {'loginName': 18291900215, 'password': 'dd636482aca022', 'code': None, 'descriptio
    4. n': 'encrypt'}}
    5. INFO 2020-07-30 22:08:17,204 request.py:55 <Response [200]>
    6. INFO 2020-07-30 22:08:17,204 request.py:56 Response Data: {"data":"1500000100070008427","result":"success"}
    7. INFO 2020-07-30 22:08:17,207 setResult.py:20 提取变量data=1500000100070008427
    8. INFO 2020-07-30 22:08:17,307 checkResult.py:18 预期响应码:200,实际响应码:200
    9. INFO 2020-07-30 22:08:17,308 checkResult.py:23 预期响应值:"result":"success",实际响应值:{"data":"1500000100070008427","result":"success"}
    10. INFO 2020-07-30 22:08:17,310 checkResult.py:29 预期正则:[\d]{16},响应['1500000100070008']
    11. INFO 2020-07-30 22:08:17,356 checkResult.py:32 用例【登录】测试成功!
    12. ok test_api_0__ (test_api.TestApi)
    13. INFO 2020-07-30 22:08:17,358 regular.py:20 提取变量:['data']
    14. INFO 2020-07-30 22:08:17,359 regular.py:23 替换变量:data
    15. INFO 2020-07-30 22:08:17,361 regular.py:26 替换结果:{"data": {"userId": "1500000100070008427"}}
    16. INFO 2020-07-30 22:08:17,363 request.py:40 Request Url: https://www.zhixue.com/loginSuccess/
    17. INFO 2020-07-30 22:08:17,366 request.py:41 Request Method: POST
    18. INFO 2020-07-30 22:08:17,367 request.py:42 Request Data: {'data': {'userId': '1500000100070008427'}}
    19. INFO 2020-07-30 22:08:20,850 request.py:55 <Response [200]>
    20. INFO 2020-07-30 22:08:20,851 request.py:56 Response Data: {"result":"success"}
    21. INFO 2020-07-30 22:08:20,932 checkResult.py:18 预期响应码:200,实际响应码:200
    22. INFO 2020-07-30 22:08:20,933 checkResult.py:23 预期响应值:"result":"success",实际响应值:{"result":"success"}
    23. INFO 2020-07-30 22:08:20,935 checkResult.py:29 预期正则:11,响应[]
    24. F test_api_1__ (test_api.TestApi)
    25. Time Elapsed: 0:00:28.281434
    26. 测试结果邮件发送成功!

    执行规则

    1. (venv) C:\Users\hoou\PycharmProjects\httptest-excel>python run.py -h
    2. usage: run.py [-h] [-i I] [-o O] [-html HTML]
    3. 运行Excel接口测试
    4. optional arguments:
    5. -h, --help show this help message and exit
    6. -i I 原始文件
    7. -o O 输出文件
    8. -html HTML 报告文件

    SHELL 复制 全屏

    在命令行输入python run.py excel路径 新excel路径 报告路径

    如果不输入新excel路径报告路径,会在run.py所在目录生成两个report.xlsx,report.html。

    本篇的excel测试框架就完成了。

    Python接口自动化测试零基础入门到精通(2023最新版)

  • 相关阅读:
    Python代码写好了怎么运行
    基于window10的远程桌面报错:要求的函数不受支持 的问题解决方法
    阿里云ESS弹性伸缩的实例配置以及伸缩组规则配置
    软件性能测试包括哪些内容?国内权威软件检测机构排名
    【工具 & 技巧 & 笔试】PyCharm搜索快捷键大总结 | 【笔试题分享】2023美团算法策略方向题目 解析思路及实例代码(含时间复杂度分析)| 动态规划 求解:人在地图中行走 获得最大金币数量
    《Python+Kivy(App开发)从入门到实践》自学笔记:简单UX部件——TextInput输入框
    Go代码包与引入:如何有效组织您的项目
    剑指offer(C++)-JZ38:字符串的排列(算法-搜索算法)
    vue修改node_modules打补丁步骤和注意事项
    「Java 数据结构」:手撕单链表的增删改查及大厂面试题。
  • 原文地址:https://blog.csdn.net/dq565/article/details/134060354