读后笔记 -- 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="") 的 作用:

  1. "function"(默认值):fixture 只在其所在函数中生效。当 fixture 所在的函数被调用时,fixture 才会被执行。
  2. "module":fixture 在整个模块中都有效。当模块被导入时,fixture 就会被执行一次。在模块中的任何测试函数都可以访问该 fixture。
  3. "package":fixture 在整个包中都有效。当包中的任意模块被导入时,fixture 就会被执行一次。在包中的任何测试函数都可以访问该 fixture。
  4. "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__)

 

posted on 2023-02-04 13:08  bruce_he  阅读(27)  评论(0编辑  收藏  举报