软件测试学习笔记丨Pytest+Allure测试计算器

编程有点难不难 2024-09-06 11:35:10

本文转自测试人社区,原文链接:

项目要求3.1 项目简介

计算器是近代人发明的可以进行数字运算的机器。 计算器通过对加法、减法、乘法、除法等功能的运算,将正确的结果展示在屏幕上。 可帮助人们更方便的实现数字运算。一般情况下计算器除显示计算结果外,还常有溢出指示、错误指示等功能。

3.1.1 知识点

测试流程与需求分析bug 提交与管理Pytest 测试框架基本用法参数化异常处理标签、跳过用例结合 Allure 生成测试报告与项目总结数据驱动pytest fixture 实现测试装置及参数化pytest conftest.py 的用法pytest 文件配置 pytest.ini使用第三方插件控制用例的执行顺序,分布式并发执行使用分层思想,实现框架的合理构建了解内置插件 hook 体系,实现插件开发

3.1.2 受众

资深测试工程师

3.1.3 作业内容

完整的测试流程,包含需求分析、测试计划设计、测试用例编写、测试执行、bug 的提交与管理。使用思维导图完成需求分分析;提供完整测试计划模板,完成测试计划设计;应用多种测试用例设计方法,包括:等价类、边界值、错误推测法等。测试执行过程中应用多种测试方法完成计算器的加法、除法运算。结合项目管理工具完成 bug 的提交与管理,进行测试报告编写与项目总结。编写自动化测试用例,结合 Allure 与截图技术等自动生成带截图与操作步骤的测试报告。使用参数化减少代码量,提高代码的可维护性。使用 mark 标签为测试用例分类设置跳过、预期失败用例对异常用例进行处理掌握 Pytest 常用的装饰器,例如:添加标签、参数化、Fixture 等。掌握 Pytest 自动化测试框架多种复杂配置,比如 pytest.ini 配置、conftest.py 配置等。合理使用第三方插件,控制测试用例的执行顺序、分布式并发执行等场景。掌握分层思想实现用例的分层,实现测试装置,测试数据,测试日志,测试报告等合理的框架构建。开发一个插件,实现命令行传递参数

3.1.4 被测源码

class Calculator: def add(self, a, b): if a > 99 or a < -99 or b > 99 or b < -99: print("请输入范围为【-99, 99】的整数或浮点数") return "参数大小超出范围" return a + b def div(self, a, b): if a > 99 or a < -99 or b > 99 or b < -99: print("请输入范围为【-99, 99】的整数或浮点数") return "参数大小超出范围" return a / b3.2 实现过程

3.2.1 代码提交

3.2.2 需求分析

3.2.3 测试计划(简版)

3.2.4 测试用例设计

3.2.5 Pytest自动化测试设计

utils.pyfrom pandas.tests.io.excel.test_openpyxl import openpyxlclass Utils: @classmethod def get_excel_data(cls, excel_path, sheetname): """ 读取 excel文件中指定 sheet 页的数据 :param excel_path: excel文件的路径 :param sheetname: sheet页的名称 :return: 返回读取的数据 """ # 打开 Excel 文件 book = openpyxl.load_workbook(excel_path) # 获取指定名称的工作表 sheet = book[sheetname] # 初始化一个空列表,用于存储行数据 values = [] for row in sheet.iter_rows(values_only=True): values.append(row) return valuestest_add_excel.pyimport sysimport pytestimport osimport allurefrom Calculator_Project.base.base import Basefrom Calculator_Project.utils.log_util import loggerfrom Calculator_Project.utils.utils import Utils# 定义一个获取加法数据的函数,接收一个参数 level@pytest.mark.parametrize("type", ["有效等价类", "无效等价类", "边界值", "错误推测"] )def get_add_data(type): """ 读取加法的测试数据 :param type: 用例类型 :return: 对应优先级的测试数据和 ids """ # 获取当前文件的系统路径 root_path = os.path.dirname(os.path.abspath(__file__)) # 打印当前文件的系统路径 print(f"当前系统路径:{root_path}") # 拼接 Excel文件的路径 excel_path = os.sep.join([root_path, '..', 'datas', 'calculator.xlsx']) # 打印 Excel文件的路径 print(f"excel文件路径:{excel_path}") sheetname = "加法" # 获取 Excel 数据 excel_data = Utils.get_excel_data(excel_path, sheetname) # 打印 Excel 数据 print(excel_data) # 初始化空列表,用于存储对应的测试数据 datas = [] ids = [] # 遍历数据行,跳过标题行 for row in excel_data[1:]: # 获取对应 level 优先级的数据 if row[0] == type: datas.append([row[1], row[2], row[3]]) # 返回 a, b, expect ids.append(row[4]) # 返回 ids # 返回测试数据和测试标题的数组 return [datas, ids]# 设置allure报告的模块名称@allure.epic("计算器测试")# 设置allure报告的模块名称@allure.feature("计算器加法测试")# 定义一个加法测试类,继承自Base类class TestAddExcel(Base): # 设置测试用例的执行顺序 @pytest.mark.run(order=1) # 设置allure报告的用户故事 @allure.story("加法-有效等价类的用例") # 使用 pytest的参数化装饰器,将get_add_datas("有效等价类")的结果解包成多个参数值,生成测试用例 @pytest.mark.parametrize( "a, b, expect", get_add_data("有效等价类")[0], ids=get_add_data("有效等价类")[1] ) # 定义一个测试加法的方法,接收3个参数:a, b, expect def test_add_valid(self, a, b, expect): # 添加图片 allure.attach.file("./datas/pic2.jpg", name="计算器图片2", attachment_type=allure.attachment_type.JPG, extension="JGP") # 记录测试用例参数的日志信息 logger.info(f"a={a}, b={b}, expect={expect}") # 设置allure报告的测试步骤 with allure.step(f"1. 调用被测程序进行计算{a}+{b}={expect}"): # 测试步骤:调用被测应用进行计算 result = self.calculator.add(a, b) # 记录计算结果的日志信息 logger.info(f"实际计算结果为:{result}") # 设置allure报告的测试步骤 with allure.step(f"2. 断言{result} == {expect}"): # 断言实际结果是否与预期结果一致 assert result == expect # 设置测试用例的执行顺序 @pytest.mark.run(order=4) # 设置allure报告的用户故事 @allure.story("加法-无效等价类的用例") # 使用 pytest的参数化装饰器,将get_add_datas("无效等价类")的结果解包成多个参数值,生成测试用例 @pytest.mark.parametrize( "a,b,expect", get_add_data("无效等价类")[0], ids=get_add_data("无效等价类")[1] ) # 定义一个测试加法的方法,接收3个参数:a, b, expect def test_add_unvalid(self, a, b, expect): # 记录测试用例参数的日志信息 logger.info(f"a={a}, b={b}, expect={expect}") # 设置allure报告的测试步骤 with allure.step(f"1. 调用被测程序进行计算{a}+{b}={expect}"): # 测试步骤:调用被测应用进行计算 result = self.calculator.add(a, b) # 记录计算结果的日志信息 logger.info(f"实际计算结果为:{result}") # 设置allure报告的测试步骤 with allure.step(f"2. 断言{result} == {expect}"): # 断言实际结果是否与预期结果一致 assert result == expect # 设置测试用例的执行顺序 @pytest.mark.run(order=2) # 设置allure报告的用户故事 @allure.story("加法-边界值的用例") # 使用 pytest的参数化装饰器,将get_add_datas("边界值")的结果解包成多个参数值,生成测试用例 @pytest.mark.parametrize( "a, b, expect", get_add_data("边界值")[0], ids=get_add_data("边界值")[1] ) # 定义一个测试加法的方法,接收3个参数:a, b, expect def test_add_boundary(self, a, b, expect): # 记录测试用例参数的日志信息 logger.info(f"a={a}, b={b}, expect={expect}") # 设置allure报告的测试步骤 with allure.step(f"1. 调用被测程序进行计算{a}+{b}={expect}"): # 测试步骤:调用被测应用进行计算 result = self.calculator.add(a, b) # 记录计算结果的日志信息 logger.info(f"实际计算结果为:{result}") # 设置allure报告的测试步骤 with allure.step(f"2. 断言{result} == {expect}"): # 断言实际结果是否与预期结果一致 assert result == expect # 设置测试用例的执行顺序 @pytest.mark.run(order=3) # 设置allure报告的用户故事 @allure.story("加法-错误推测的用例") # 使用 pytest的参数化装饰器,将get_add_datas("错误推测")的结果解包成多个参数值,生成测试用例 @pytest.mark.parametrize( "a,b,expect", get_add_data("错误推测")[0], ids=get_add_data("错误推测")[1] ) # 定义一个测试加法的方法,接收3个参数:a, b, expect def test_add_type_error(self, a, b, expect): # 记录测试用例参数的日志信息 logger.info(f"a={a}, b={b}, expect={expect}") # 捕获预期的异常 with pytest.raises(eval(expect)) as e: # 设置allure报告的测试步骤 with allure.step(f"1. 调用被测程序进行计算{a}+{b}={expect}"): # 测试步骤:调用被测应用进行计算 result = self.calculator.add(a, b) # 记录计算结果的日志信息 logger.info(f"实际计算结果为:{result}") # 记录捕获的异常信息 logger.info(f"类型错误为:{e}") # 设置allure报告的测试步骤 with allure.step(f"2. 断言{e.type} == {TypeError}"): assert e.type == TypeError # 设置测试用例的执行顺序 @pytest.mark.run(order=5) # 设置始终跳过的测试用例 @pytest.mark.skip def test_error_message(self): print("详细提示信息未完成开发,暂不测试。") assert True # 设置测试用例的执行顺序 @pytest.mark.run(order=7) # 设置特定条件下跳过的测试用例 @pytest.mark.skipif(sys.platform == "darwin", reson = "不做mac的兼容") def test_sys(self): print("Mac系统不做兼容测试") assert True # 设置测试用例的执行顺序 @pytest.mark.run(order=6) # 预期失败的测试用例 @pytest.mark.xfail def test_fail(self): print("标记期望失败的案例,还没想好哪些失败。。。") assert Truetest_div_excel.pyimport sysimport pytestimport osimport allurefrom Calculator_Project.base.base import Basefrom Calculator_Project.utils.log_util import loggerfrom Calculator_Project.utils.utils import Utils# 定义一个获取除法数据的函数,接收一个参数 level@pytest.mark.parametrize("type", ["有效等价类", "无效等价类", "边界值", "错误推测", "被除数为0"] )def get_div_data(type): """ 读取除法的测试数据 :param type: 用例类型 :return: 对应优先级的测试数据和 ids """ # 获取当前文件的系统路径 root_path = os.path.dirname(os.path.abspath(__file__)) # 打印当前文件的系统路径 print(f"当前系统路径:{root_path}") # 拼接 Excel文件的路径 excel_path = os.sep.join([root_path, '..', 'datas', 'calculator.xlsx']) # 打印 Excel文件的路径 print(f"excel文件路径:{excel_path}") # 获取 Excel 数据 excel_data = Utils.get_excel_data(excel_path, "除法") # 打印 Excel 数据 print(excel_data) # 初始化空列表,用于存储对应的测试数据 datas = [] ids = [] # 遍历数据行,跳过标题行 for row in excel_data[1:]: # 获取对应 level 优先级的数据 if row[0] == type: datas.append([row[1], row[2], row[3]]) # 返回 a, b, expect ids.append(row[4]) # 返回 ids # 返回测试数据和测试标题的数组 return [datas, ids]# 设置allure报告的模块名称@allure.epic("计算器测试")# 设置allure报告的模块名称@allure.feature("计算器除法测试")# 定义一个除法测试类,继承自Base类class TestDivExcel(Base): # 设置测试用例的执行顺序 @pytest.mark.run(order=1) # 设置allure报告的用户故事 @allure.story("除法-有效等价类的用例") # 使用 pytest的参数化装饰器,将get_div_datas("有效等价类")的结果解包成多个参数值,生成测试用例 @pytest.mark.parametrize( "a, b, expect", get_div_data("有效等价类")[0], ids=get_div_data("有效等价类")[1] ) # 定义一个测试除法的方法,接收3个参数:a, b, expect def test_div_valid(self, a, b, expect): # 添加图片 allure.attach.file("./datas/pic1.jpg", name="计算器图片1", attachment_type=allure.attachment_type.JPG, extension="JGP") # 记录测试用例参数的日志信息 logger.info(f"a={a}, b={b}, expect={expect}") # 设置allure报告的测试步骤 with allure.step(f"1. 调用被测程序进行计算{a}/{b}={expect}"): # 测试步骤:调用被测应用进行计算 result = self.calculator.div(a, b) # 记录计算结果的日志信息 logger.info(f"实际计算结果为:{result}") # 设置allure报告的测试步骤 with allure.step(f"2. 断言{result} == {expect}"): # 断言实际结果是否与预期结果一致 assert result == expect # 设置测试用例的执行顺序 @pytest.mark.run(order=4) # 设置allure报告的用户故事 @allure.story("除法-无效等价类的用例") # 使用 pytest的参数化装饰器,将get_div_datas("无效等价类")的结果解包成多个参数值,生成测试用例 @pytest.mark.parametrize( "a,b,expect", get_div_data("无效等价类")[0], ids=get_div_data("无效等价类")[1] ) # 定义一个测试除法的方法,接收3个参数:a, b, expect def test_div_unvalid(self, a, b, expect): # 记录测试用例参数的日志信息 logger.info(f"a={a}, b={b}, expect={expect}") # 设置allure报告的测试步骤 with allure.step(f"1. 调用被测程序进行计算{a}/{b}={expect}"): # 测试步骤:调用被测应用进行计算 result = self.calculator.div(a, b) # 记录计算结果的日志信息 logger.info(f"实际计算结果为:{result}") # 设置allure报告的测试步骤 with allure.step(f"2. 断言{result} == {expect}"): # 断言实际结果是否与预期结果一致 assert result == expect # 设置测试用例的执行顺序 @pytest.mark.run(order=2) # 设置allure报告的用户故事 @allure.story("除法-边界值的用例") # 使用 pytest的参数化装饰器,将get_div_datas("边界值")的结果解包成多个参数值,生成测试用例 @pytest.mark.parametrize( "a, b, expect", get_div_data("边界值")[0], ids=get_div_data("边界值")[1] ) # 定义一个测试除法的方法,接收3个参数:a, b, expect def test_div_boundary(self, a, b, expect): # 记录测试用例参数的日志信息 logger.info(f"a={a}, b={b}, expect={expect}") # 设置allure报告的测试步骤 with allure.step(f"1. 调用被测程序进行计算{a}/{b}={expect}"): # 测试步骤:调用被测应用进行计算 result = self.calculator.div(a, b) # 记录计算结果的日志信息 logger.info(f"实际计算结果为:{result}") # 设置allure报告的测试步骤 with allure.step(f"2. 断言{result} == {expect}"): # 断言实际结果是否与预期结果一致 assert result == expect # 设置测试用例的执行顺序 @pytest.mark.run(order=3) # 设置allure报告的用户故事 @allure.story("除法-错误推测的用例") # 使用 pytest的参数化装饰器,将get_div_datas("错误推测")的结果解包成多个参数值,生成测试用例 @pytest.mark.parametrize( "a,b,expect", get_div_data("错误推测")[0], ids=get_div_data("错误推测")[1] ) # 定义一个测试除法的方法,接收3个参数:a, b, expect def test_div_type_error(self, a, b, expect): # 记录测试用例参数的日志信息 logger.info(f"a={a}, b={b}, expect={expect}") # 捕获预期的异常 with pytest.raises(eval(expect)) as e: # 设置allure报告的测试步骤 with allure.step(f"1. 调用被测程序进行计算{a}/{b}={expect}"): # 测试步骤:调用被测应用进行计算 result = self.calculator.div(a, b) # 记录计算结果的日志信息 logger.info(f"实际计算结果为:{result}") # 记录捕获的异常信息 logger.info(f"类型错误为:{e}") # 设置allure报告的测试步骤 with allure.step(f"2. 断言{e.type} == {TypeError}"): assert e.type == TypeError # 设置测试用例的执行顺序 @pytest.mark.run(order=5) # 设置allure报告的用户故事 @allure.story("除法-被除数为0的用例") # 使用 pytest的参数化装饰器,将get_div_datas("被除数为0")的结果解包成多个参数值,生成测试用例 @pytest.mark.parametrize( "a, b, expect", get_div_data("被除数为0")[0], ids=get_div_data("被除数为0")[1] ) # 定义一个测试除法的方法,接收3个参数:a, b, expect def test_div_zero(self, a, b, expect): # 记录测试用例参数的日志信息 logger.info(f"a={a}, b={b}, expect={expect}") # 捕获预期的异常 with pytest.raises(eval(expect)) as e: # 设置allure报告的测试步骤 with allure.step(f"1. 调用被测程序进行计算{a}/{b}={expect}"): # 测试步骤:调用被测应用进行计算 result = self.calculator.div(a, b) # 记录计算结果的日志信息 logger.info(f"实际计算结果为:{result}") # 记录捕获的异常信息 logger.info(f"类型错误为:{e}") # 设置allure报告的测试步骤 with allure.step(f"2. 断言{e.type} == ZeroDivisionError"): assert e.type == ZeroDivisionError # 设置测试用例的执行顺序 @pytest.mark.run(order=8) # 设置始终跳过的测试用例 @pytest.mark.skip def test_error_message(self): print("详细提示信息未完成开发,暂不测试。") assert True # 设置测试用例的执行顺序 @pytest.mark.run(order=7) # 设置特定条件下跳过的测试用例 @pytest.mark.skipif(sys.platform == "darwin", reson = "不做mac的兼容") def test_sys(self): print("Mac系统不做兼容测试") assert True # 设置测试用例的执行顺序 @pytest.mark.run(order=6) # 预期失败的测试用例 @pytest.mark.xfail def test_fail(self): print("标记期望失败的案例,还没想好哪些失败。。。") assert Trueconftest.pyimport pytestimport yaml# 解决用例描述中的中文乱码问题# 定义一个pytest的钩子函数,接收3个参数 session, config, items,返回值类型都为 None# 主要用于在收集测试用例后,对其进行修改def pytest_collection_modifyitems(session, config, items): # 循环遍历所有收集到的测试用例的 items for item in items: # 对每个 item的 name进行编码和解码 # item.name.encode('utf-8')是将 item按照 UTF-8格式编码 # decode("unicode-escape")是将编码后的字符串按照 unicode-escape格式解码,以便正确显示中文字符。 item.name = item.name.encode("utf-8").decode("unicode-escape") # 对每个 item的 _nodeid进行编码和解码 item._nodeid = item._nodeid.encode("utf-8").decode("unicode-escape")pytest.ini[pytest]# 日志开关 true falselog_cli = true# 日志级别log_cli_level = info# 使用4个并发进程运行测试# 打印详细日志,相当于命令行加 -vs# 在第3个失败的测试后停止执行addopts = -n 4 --capture=no --maxfail=3# 日志格式log_cli_format = %(asctime)s [%(levelname)s] %(message)s (%(filename)s:%(lineno)s)# 日志时间格式log_cli_date_format = %Y-%m-%d %H:%M:%S# 日志文件位置log_file = ./log/test.log# 日志文件等级log_file_level = info# 日志文件格式log_file_format = %(asctime)s [%(levelname)s] %(message)s (%(filename)s:%(lineno)s)# 日志文件日期格式log_file_date_format = %Y-%m-%d %H:%M:%# 设置执行的路径testpaths = tests# 匹配测试文件的命名模式python_files = test_*.py# 匹配测试类的命名模式python_classes = Test*# 匹配测试函数的命名模式python_functions = test_*markers = run: specify order of test exectionlog_util.pyimport loggingimport osfrom logging.handlers import RotatingFileHandler# 绑定绑定句柄到logger对象logger = logging.getLogger(__name__)# 获取当前工具文件所在的路径root_path = os.path.dirname(os.path.abspath(__file__))# 拼接当前要输出日志的路径log_dir_path = os.sep.join([root_path, '..', f'/logs'])if not os.path.isdir(log_dir_path): os.mkdir(log_dir_path)# 创建日志记录器,指明日志保存路径,每个日志的大小,保存日志的上限file_log_handler = RotatingFileHandler(os.sep.join([log_dir_path, 'log.log']), maxBytes=1024 * 1024, backupCount=10)# 设置日志的格式date_string = '%Y-%m-%d %H:%M:%S'formatter = logging.Formatter( '[%(asctime)s] [%(levelname)s] [%(filename)s]/[line: %(lineno)d]/[%(funcName)s] %(message)s ', date_string)# 日志输出到控制台的句柄stream_handler = logging.StreamHandler()# 将日志记录器指定日志的格式file_log_handler.setFormatter(formatter)stream_handler.setFormatter(formatter)# 为全局的日志工具对象添加日志记录器# 绑定绑定句柄到logger对象logger.addHandler(stream_handler)logger.addHandler(file_log_handler)# 设置日志输出级别logger.setLevel(level=logging.INFO)

3.2.6 Allure报告

在线Allure报告

静态Allure报告

0 阅读:0

编程有点难不难

简介:感谢大家的关注