读后笔记 -- Python 全栈测试开发 Chapter12:pytest框架 + Allure 报告生成
12.1 pytest 框架
12.1.1 简介
1. 框架的使用程度:pytest > unittest > RF 框架 > 无框架的 关键字驱动 > 数据驱动 > 线性脚本
pytest 在线文档:https://docs.pytest.org/en/7.2.x/
2. pytest 相比较于 unittest:
- 简单:摒弃大量的必须步骤;
- 新增简单易用的操作(测试固件的自定义)等
3. pytest 运行测试用例的几种方式:
// Calculator.py class Calculator(object): def add(self, a, b): return a + b
// test_calculator.py import pytest from Ch12_PytestAllure.Ch12_1_1.SourceDir.Calculator import Calculator class TestCalculator(): def test_add(self): calculator = Calculator() assert 4 == calculator.add(2, 2) if __name__ == '__main__': pytest.main()
1. DOS 下运行 (DOS) > pytest 测试用例.py pytest C:\Users\Google_he\PycharmProjects\trunk\LessonOfPythonFullStackTestDev\Ch12_PytestAllure\Ch12_1_1\TestDir\test_calculator.py (DOS) > py.test 模块路径。 py.test C:\Users\Google_he\PycharmProjects\trunk\LessonOfPythonFullStackTestDev\Ch12_PytestAllure\Ch12_1_1\TestDir (DOS) > python -m pytest 模块路径。 python -m pytest C:\Users\Google_he\PycharmProjects\trunk\LessonOfPythonFullStackTestDev\Ch12_PytestAllure\Ch12_1_1\TestDir 2. pycharm (需要 pycharm 修改配置: \Settings\Tools\Python Integrated Tools\Testing\Default test runner: pytest) 3. DOS 下加参数 -v / -vv 显示断言结果的差异: (DOS) > pytest -v/-vv 测试用例.py pycharm: 通过 Edit Configuration\arguments: 添加 "-vv" 可实现相同的结果 4. 只运行指定的测试函数 4.1 pycharm: 只点击指定的 test 方法名,运行 4.2 DOS> pytest 测试用例.py::测试类名::测试方法名
4. pytest 的规范:
- 测试类:必须以 “Test” 开头 (T 必须大写;放到中间,不被识别为 pytest 模块;放在后面,该测试用例不会被检测到)
- 测试方法:必须以 “test” 开头
- 模块名称:建议以 "Test" 开头或结尾,方便后期批量执行
5. assert 断言方法:
- 1)assert bool 表达式:断言当前结果是否真、假
- 2)assert 实际结果 == 预期结果:判断两个结果的值是否相同,其他如 <, >, !=, <=, >= 等
- 3)assert A is B:判断A和B是否同一对象
- 4)assert isinstance(a, A):判断a 是否 A 的实例
- 5)assert str1 in list1:判断 str1 是否包含在 list1 内容中
- 6)特别 -- 异常断言,使用 with pytest.raises() as excinfo,获取异常的实例,然后断言 excinfo.value / .type / .traceback
import pytest def divide(a, b): return a / b class TestAA: # 断言异常,传统方式--捕获异常进行断言 def test_error(self): try: divide(1, 0) except: get_info = sys.exc_info() print(get_info) get_expect_object = get_info[0] get_message = get_info[1] print(get_expect_object) assert get_expect_object == ZeroDivisionError # 实际想要的断言方式, def test_divide_actualwant(self): with pytest.raises(ZeroDivisionError) as excinfo: divide(1, 0) assert str(excinfo.value) == "division by zero" assert ZeroDivisionError == excinfo.type if __name__ == '__main__': pytest.main()
12.1.2 模块级、函数级、类级、方法级
1. 测试对象初始化:在 pytest 中其对象处理及初始化操作具有 4 个级别:
- 模块级别:(setup_module / teardown_module,注意大小写):针对整个模块而言,在整个模块中所有测试用例执行前仅执行一次(针对函数 及 类中的测试用例,如需要共用同一个对象,就可以设计成模块级别)。一般写在类外面
- 函数级别(setup_function / teardown_function,注意大小写):针对函数而言,在每个函数测试用例执行之前会执行一次;setup_function 定义的对象是通过全局变量进行传递;一般写在类外面
- 类级别(setup_class / teardown_class,注意大小写):等价于 unittest 框架中的 setUpClass 和 tearDownClass。表示在测试类中的所有测试用例执行之前仅执行一次;写在类里面
- 方法(类中的)级别(setup_method / teardown_method,注意大小写):等价于 unittest 框架中的 setUp 和 tearDown。表示在测试类中的每个测试用例执行之前会执行一次。一般写在 类里面;
2. 测试用例执行顺序:按照顺序执行,不区分函数、类。(区别于 unittest 的 ASCII 码)
3. 自定义固件对象:可不受 上述 方法名的约束,以及调用时可多样式。通过 pytest.fixture 对象完成装饰固件对象,从而声明 当前函数是一个固件函数
针对自定义 fixture 函数调用的方式:
- 1. 直接作为测试函数或类的测试方法中的参数进行传入;
- 2. 使用 pytest.mark.usefixture 装饰测试函数或测试方法。注意:该方法的测试固件需要通过全局变量来进行传递。
- 3. 通过测试固件自带属性 autouse = True 完成固件的应用操作;不需要像第二种方式那样使用装饰器。注意:该方法的测试固件需要通过全局变量来进行传递。
// 方式1 的用例:目的:可以将公共操作独立出来 // conftest.py import pytest
@pytest.fixture() def login(): print("前置进入输入账号密码登录")
# 如 test_a1(login),其执行过程是 执行该方法的第一个print -> 执行 test_a1 -> 执行该方法的第二个print
yield
print("执行 tearDown 的动作") // test_conftest_method1_1.py import pytest def test_a1(login): print("执行测试用例1,继承 login函数,前置登录后,继续执行下一步操作") def test_a2(): print("执行测试用例2,不需要登录,继续执行下一步操作") def test_a3(login): print("执行测试用例3,继承 login函数,前置登录后,继续执行下一步操作") if __name__ == '__main__': pytest.main() // test_conftest_method1_2 import pytest def test_a4(login): print("执行测试用例1,继承 login函数,前置登录后,继续执行下一步操作") def test_a5(): print("执行测试用例2,不需要登录,继续执行下一步操作") if __name__ == '__main__': pytest.main()
// 方式2 的用例 import pytest from Ch12_PytestAllure.Ch12_1_2.SourceDir.Calculator import Calculator get_cal_1 = None @pytest.fixture() def get_cal_1(): global get_cal_1 # 定义成 global,那么普通方法 test_add_1 和 类中的方法 test_add_2 都可以引用对象 get_cal_1 get_cal_1 = Calculator() @pytest.mark.usefixtures("get_cal_1") def test_add_1(): assert 4 == get_cal_1.add(2, 2) class TestCalculator(): def test_add_2(self): assert 5 == get_cal_1.add(1, 4)
// 方式3 的用例 import pytest from Ch12_PytestAllure.Ch12_1_2.SourceDir.Calculator import Calculator get_cal_2 = None @pytest.fixture(autouse=True) def get_cal_2(): global get_cal_2 get_cal_2 = Calculator() def test_add_3(): assert 4 == get_cal_2.add(1, 3) if __name__ == '__main__': pytest.main()
12.1.4 conftest.py 的作用域
@pytest.fixture(scope="") 的 作用:
- "function"(默认值):fixture 只在其所在函数中生效。当 fixture 所在的函数被调用时,fixture 才会被执行。
- "module":fixture 在整个模块中都有效。当模块被导入时,fixture 就会被执行一次。在模块中的任何测试函数都可以访问该 fixture。
- "package":fixture 在整个包中都有效。当包中的任意模块被导入时,fixture 就会被执行一次。在包中的任何测试函数都可以访问该 fixture。
- "session":fixture 在整个 pytest 会话中都有效。无论何时,只要 pytest 会话开始,fixture 就会被执行一次。所有的测试函数都可以访问该 fixture。
在 pytest 中,function、module、package 和 session 都有各自的应用场景,具体如下:
function:当需要定义一个独立的测试函数时,可以使用 function 参数。测试函数是 pytest 中最基本的功能单元,用于测试被测代码的每个功能或行为。
module:当需要将多个测试函数组织成一个测试模块时,可以使用 module 参数。测试模块是一个包含多个测试函数的 Python 文件,通常用于将相关的测试函数分组。
package:当需要将多个测试模块组织成一个测试包时,可以使用 package 参数。测试包是一个包含多个测试模块的目录,通常用于将相关的测试模块分组。
session:当需要在一个独立的 pytest 会话中运行多个测试套件时,可以使用 session 参数。pytest 会话是从 pytest 命令行启动开始到所有测试运行结束的时间段。使用 session 参数可以创建多个独立的 pytest 会话,以便在每个会话中运行不同的测试用例或测试套件。
更详细的描述,参看 https://www.cnblogs.com/bruce-he/p/17832347.html
12.1.5 多个 fixture
传入多个 fixture 对象有两种方式:
- 1. 以 元组、列表或字典等容器的形式存储一个需要返回多个数据的 fixture 对象;
- 2. 将 多个数据分为每个 fixture 对象进行返回;
# conftest.py import pytest from Ch12_PytestAllure.Ch12_1_5.SourceDir.Calculator import Calculator @pytest.fixture(scope="session") def get_cal(): print("当前包下的测试固件对象") return Calculator()
# 方式1:以 元组、列表或字典等容器的形式存储一个需要返回多个数据的 fixture 对象 import pytest @pytest.fixture() def get_number(): print("获取加法的2个数") a = 1 b = 2 # 将多个对象拼接成 元组() 的形式 return (a, b) # get_number 以参数化的形式传入,get_cal 是来自 conftest.py 中,也是以参数的形式传入 def test_1(get_number, get_cal): # 将 get_number 返回的元组再拆解成两个变量 a = get_number[0] b = get_number[1] assert get_cal.add(a, b) == 3 if __name__ == '__main__': pytest.main()
# 方式 2:将 多个数据分为每个 fixture 对象进行返回 import pytest @pytest.fixture() def get_a(): print("获取加法的a") a = 1 return a @pytest.fixture() def get_b(): print("获取加法的b") b = 2 return b def test_1(get_a, get_b, get_cal): assert get_cal.add(get_a, get_b) == 3 if __name__ == '__main__': pytest.main()
12.1.8 fixture 的用例管理
# 用例包含: skip, skipif, xfail, repeat
import sys import pytest @pytest.mark.skip(reason="指定原因跳过") def test_skip(): pass @pytest.mark.skipif(sys.version_info[0: 3] > (3, 8), reason="版本高于 3.8 不执行") def test_skipif(get_cal): assert get_cal.add(44, 4) == 48 @pytest.mark.xfail(condition=False, reason="当前测试对象不存在") def test_xfail(get_cal_function): assert get_cal_fuction.add(1, 2) == 3 @pytest.mark.xfail() def test_xfail1(): print("预期结果失败,实际结果成功。将显示为'XPASS' ") assert 1 == 1 @pytest.mark.xfail() def test_xfail2(): print("预期结果失败,实际结果失败。将显示为'XFAIL' ") assert 1 == 0 @pytest.mark.repeat(5) def test_repeat(): assert 1 == 1 if __name__ == '__main__': pytest.main()
12.2 pytest 高级应用及技术
1. pytest.fixture 携带参数
import pytest from Ch12_PytestAllure.Ch12_2_1.SourceDir.Calculator import Calculator @pytest.fixture(scope="function", params=[[1, 2, 3], [2, 2, 4]]) # 如果使用 params 实现参数化,那么:1)传参固定是 request;2)返回参数对象必须是 request.param def get_calculator(request): return Calculator(), request.param def test_add(get_calculator): # get_calculator[0]: 就是 Calculator() 的对象 assert get_calculator[0].add(get_calculator[1][0], get_calculator[1][1]) == get_calculator[1][2] if __name__ == '__main__': pytest.main()
2. pytest.fixture 读取 yaml 文件
# data.yaml calculator_add: - [1, 1, 2] - [2, 2, 4] - [3, 3, 6] calculator_add_1: - {"a": 1, "b": 1, "c": 2}
# conftest.py import pytest from Ch12_PytestAllure.Ch12_2_1.SourceDir.Calculator import Calculator from Ch12_PytestAllure.Ch12_2_1.TestDir.read_yaml import ReadYaml # 不能直接这样返回,数据不会解包,无法使用 @pytest.fixture(scope="function") def get_yaml(): return ReadYaml().read_yaml()["calculator_add"] # 要完成解包,还是需要通过 params 入参,通过 request.param 返回 @pytest.fixture(scope="function", params=ReadYaml().read_yaml()['calculator_add']) def get_yaml_params(request): return request.param
# test_yaml_param import pytest def test_add_3(get_calculator_2, get_yaml_params): assert get_calculator_2.add(get_yaml_params[0], get_yaml_params[1]) == get_yaml_params[2]
12.2.2 pytest 的 parameterize 参数化
# conftest.py import pytest from Ch12_PytestAllure.Ch12_2_1.SourceDir.Calculator import Calculator @pytest.fixture(scope="function") def get_calculator(): return Calculator()
# test 脚本 import pytest @pytest.mark.parametrize("a, b, c", [[1, 2, 3], [2, 2, 4]]) def test_add_with_parameterize(get_calculator, a, b, c): assert get_calculator.add(a, b) == c
12.2.3 生成 pytest 报告
# 生成 html 报告,--result-log 已经从 pytest 6.0 开始被移除 > pip install pytest-html > pytest c:\Users\Google_he\PycharmProjects\trunk\LessonOfPythonFullStackTestDev\Ch12_PytestAllure --html=e:\temp\pylog-html.html
12.2.4 pytest-html 报告的二次封装
# 可以放在 project 下的 conftest.py import pytest # 需要 "pip install py" from py.xml import html import time def pytest_html_results_table_header(cells): cells.insert(2, html.th('Description')) cells.insert(1, html.th('Time', class_='sortable time', col='time')) cells.insert(3, html.th('Y OR N')) cells.pop() def pytest_html_results_table_row(report, cells): cells.insert(2, html.td(report.description)) cells.insert(1, html.td(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), class_='col-time')) cells.insert(3, html.td("Y")) cells.pop() # 解决 用例中文显示问题 def pytest_collection_modifyitems(items): # item表示每个测试用例,解决用例名称中文显示问题 for item in items: item.name = item.name.encode("utf-8").decode("unicode-escape") item._nodeid = item._nodeid.encode("utf-8").decode("unicode-escape") # 自定义 report title def pytest_html_report_title(report): report.title = "TestCaseCh12_2_2" @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): outcome = yield report = outcome.get_result() report.description = str(item.function.__doc__)