pytest fixtures:作为函数参数,scope参数控制fixture的作用范围,conftest.py ,自动执行 fixture
数据驱动: fixture 传递参数, parametrize 参数化,pytest 结合 YAML
多线程并行与分布式执行

fixtures


fixtures源码:

def fixture(  # noqa: F811
    fixture_function: Optional[_FixtureFunction] = None,
    *,
    scope: "Union[_Scope, Callable[[str, Config], _Scope]]" = "function",
    params: Optional[Iterable[object]] = None,
    autouse: bool = False,
    ids: Optional[
        Union[
            Iterable[Union[None, str, float, int, bool]],
            Callable[[Any], Optional[object]],
        ]
    ] = None,
    name: Optional[str] = None
) -> Union[FixtureFunctionMarker, _FixtureFunction]:
    ....

更详细可参考源码中的__doc__对各参数的使用说明

在之前的文章中介绍了pytest的setup和teardown来初始化。
但如果这样的场景:用例1需要登陆,用例2不需要登陆,用例3又需要登陆。利用setup和teardown则不好处理,这就需要@pytest.fixtures()装饰器来实现

@pytest.fixtures()装饰器

  1. 示例:
import pytest

@pytest.fixture()
def login():
    print("\n这是登陆方法")
    return "\nget login token success"

@pytest.fixture()
def data_process():
    print("\n这是登陆后的数据处理")
    return "done"

def test_case1(login):
    print(login)
    print("测试用例1,需要登陆")

def test_case2():
    print("测试用例2,不需要登陆")

def test_case3(login, data_process):
    print(login)
    print("测试用例3,需要登陆")
    print(data_process)
  1. 分析
    加上 @pytest.fixture这个装饰器后,将这个用例方法名以参数的形式传到方法里,这个方法就会先执行这个登录方法(被装饰的函数),再去执行自身的用例步骤,如果没有传入这个登录方法,就不执行登录操作,直接执行已有的步骤。

@pytest.fixture中的scope参数控制fixture的作用范围

假设这样的场景:在全部用例执行之前打开浏览器,全部执行完之后去关闭浏览器,打开和关闭操作只执行一次,如果每次都重新执行打开操作,会非常占用系统资源。显然可以利用在之前的文章pytest的setup和teardown中的setup_module和teardown_module来控制实现。
这里再介绍一种:利用@pytest.fixture中的scope参数来指定执行作用范围为module,即@pytest.fixture(scope='module')

  1. 作用范围
    根据作用范围大小划分:session> module> class> function,具体作用范围如下:
  • function 函数或者方法级别都会被调用
  • class 类级别调用一次
  • module 模块级别调用一次
  • session 多个文件调用一次(可以跨.py文件调用,每个.py文件就是module)

@pytest.fixture() 如果不写参数,参数默认 scope='function'

  1. 示例
import pytest

# 作用域:module是在模块之前执行, 模块之后执行
@pytest.fixture(scope="module")
def open():
    print("打开浏览器")
    yield

    print("执行teardown !")
    print("最后关闭浏览器")

@pytest.mark.usefixtures("open")
def test_search1():
    print("test_search1")
    raise NameError
    pass

def test_search2(open):
    print("test_search2")
    pass

def test_search3(open):
    print("test_search3")
    pass
  1. 分析
    scope="module" 与 yield 结合,相当于 setup_module 和 teardown_module 方法。整个模块运行之前调用了 open()方法中 yield 前面的打印输出“打开浏览器”,整个运行之后调用了 yield 后面的打印语句“执行 teardown !”与“关闭浏览器”。yield 来唤醒 teardown 的执行,如果用例出现异常,不影响 yield 后面的 teardown 执行。可以使用 @pytest.mark.usefixtures 装饰器来进行方法的传入

conftest.py文件

fixture scope 为 session 级别是可以跨 .py 模块调用的,也就是当我们有多个 .py 文件的用例时,如果多个用例只需调用一次 fixture,可以将 scope='session',并且写到 conftest.py 文件里。写到 conftest.py 文件可以全局调用这里面的方法。使用的时候不需要导入 conftest.py 这个文件。

  • 使用 conftest.py 的规则:
  1. conftest.py 这个文件名是固定的,不可以更改
  2. conftest.py 与运行用例在同一个包下,并且该包中有 init.py 文件
  3. 使用的时候不需要导入 conftest.py,pytest 会自动识别到这个文件
  4. 放到项目的根目录下可以全局调用,放到某个 package 下,就在这个 package 内有效
  • 示例
    如下目录:
test_scope
├── __init__.py
├── conftest.py
├── test_scope1.py
└── test_scope2.py

conftest.py 与运行的用例要在同一个 pakage 下,并且这个包下有 __init__.py 文件
conftest.py:

import pytest

@pytest.fixture(scope="session")
def open():
    print("打开浏览器")
    yield

    print("执行teardown !")
    print("最后关闭浏览器")

test_scope1.py:

import pytest

def test_search1(open):
    print("test_search1")
    pass

def test_search2(open):
    print("test_search2")
    pass

def test_search3(open):
    print("test_search3")
    pass

# if __name__ == '__main__':
#     pytest.main()

test_scope2.py:

class TestFunc():
    def test_case1(self):
        print("test_case1,需要登录")

    def test_case2(self):
        print("test_case2,不需要登录 ")

    def test_case3(self):
        print("test_case3,需要登录")

进入test_scope目录,执行命令pytest -v -spytest -v -s test_scope1.py test_scope2.py
结果如下:

collected 6 items
test_scope1.py::test_search1 打开浏览器
test_search1
PASSED
test_scope1.py::test_search2 test_search2
PASSED
test_scope1.py::test_search3 test_search3
PASSED
test_scope2.py::TestFunc::test_case1 test_case1,需要登录
PASSED
test_scope2.py::TestFunc::test_case2 test_case2,不需要登录 
PASSED
test_scope2.py::TestFunc::test_case3 test_case3,需要登录
PASSED执行teardown !
最后关闭浏览器                

自动执行fixture

如果每条测试用例都需要添加 fixture 功能,则需要在每一要用例方法里面传入这个fixture的名字,这里就可以在装饰器里面添加一个参数 autouse='true',它会自动应用到所有的测试方法中,只是这里没有办法返回值给测试用例。

import pytest

@pytest.fixture(autouse="true")
def myfixture():
    print("this is my fixture")


class TestAutoUse:
    def test_one(self):
        print("执行test_one")
        assert 1 + 2 == 3

    def test_two(self):
        print("执行test_two")
        assert 1 == 1

    def test_three(self):
        print("执行test_three")
        assert 1 + 1 == 2

执行结果:

test_a.py::TestAutoUse::test_one this is my fixture
执行test_one
PASSED
test_a.py::TestAutoUse::test_two this is my fixture
执行test_two
PASSED
test_a.py::TestAutoUse::test_three this is my fixture
执行test_three
PASSED

在方法 myfixture() 上面添加了装饰器 @pytest.fixture(autouse="true"),测试用例无须传入这个 fixture 的名字,它会自动在每条用例之前执行这个 fixture

数据驱动


fixture传参数@pytest.fixture(params=)

如使用@pytest.fixture(params=[1,2,3]),就会传入三个数据 1、2、3,分别将这三个数据传入到用例当中。这里可以传入的数据是个列表。传入的数据需要使用一个固定的参数名request来接收。
示例:

import pytest

@pytest.fixture(params=[1, 2, 3])
def data(request):
    return request.param

def test_not_2(data):
    print(f"测试数据:{data}")
    assert data < 5

使用request.param来接受用例参数化的数据,并且为每一个测试数据生成一个测试结果。在测试工作中使用这种参数化的方式,会减少大量的代码量,并且便于阅读与维护。

@pytest.mark.parametrize参数化

源码:

    class _ParametrizeMarkDecorator(MarkDecorator):
        def __call__(  # type: ignore[override]
            self,
            argnames: Union[str, List[str], Tuple[str, ...]],
            argvalues: Iterable[Union[ParameterSet, Sequence[object], object]],
            *,
            indirect: Union[bool, Sequence[str]] = ...,
            ids: Optional[
                Union[
                    Iterable[Union[None, str, float, int, bool]],
                    Callable[[Any], Optional[object]],
                ]
            ] = ...,
            scope: Optional[_Scope] = ...
        ) -> MarkDecorator:
            ...

主要参数说明:

  • argsnames :参数名,字符串(若是list或tuple中的元素必须是字符串),如中间用逗号分隔则表示为多个参数名
  • argsvalues :参数值,参数组成的列表,列表中有几个元素,就会生成几条用例(与参数名对应成对)
  • indirect:若参数设置为 True,pytest 会把 argnames 当作函数去执行,将 argvalues 作为参数传入到 argnames 这个函数里

使用方法:

  1. 使用 @pytest.mark.paramtrize() 装饰测试方法
  2. parametrize('data', param) 中的 “data” 是自定义的参数名,param 是引入的参数列表
  3. 将自定义的参数名 data 作为参数传给测试用例 test_func,然后就可以在测试用例内部使用 data 的参数了
  • 示例1
import pytest
@pytest.mark.parametrize(['a', 'b'], [(1, 1), (2, 2), (3, 3)])
def test_case(a, b):
    assert a+b < 10
  • 示例2
@pytest.mark.parametrize("test_input,expected",[("3+5",8),("2+5",7),("7*5",30)])
def test_eval(test_input,expected):
    # eval 将字符串str当成有效的表达式来求值,并返回结果
    assert eval(test_input) == expected

pytest 将参数列表 [("3+5",8),("2+5",7),("7*5",30)] 中的三组数据取出来,每组数据生成一条测试用例,并且将每组数据中的两个元素分别赋值到方法中,作为测试方法的参数由测试用例使用

  • 示例3
@pytest.mark.parametrize("x",[1,2])
@pytest.mark.parametrize("y",[8,10,11])
def test_foo(x,y):
    print(f"测试数据组合x: {x} , y:{y}")

两个装饰器分别提供两个参数值的列表,2 * 3 = 6 种结合,pytest 便会生成 6 条测试用例。在测试中通常使用这种方法是所有变量、所有取值的完全组合,可以实现全面的测试。

  • 示例4
    参数值argvalues也可以是函数
import random
import pytest

def data(num):
    lst = list()
    for i in range(num):
        lst.append(random.randint(1, 100))
    return lst

@pytest.mark.parametrize("a", data(3))
def test_func(a):
    assert a != 100

@pytest.fixture 与 @pytest.mark.parametrize 结合

如果测试数据需要在 fixture 方法中使用,同时也需要在测试用例中使用,可以在使用 parametrize 的时候添加一个参数 indirect=True,pytest 可以实现将参数传入到 fixture 方法中,也可以在当前的测试用例中使用。

  • 示例
# 方法名作为参数
test_user_data = ['Tom', 'Jerry']
@pytest.fixture(scope="module")
def login_r(request):
    # 通过request.param获取参数
    user = request.param
    print(f"\n 登录用户:{user}")
    return user

@pytest.mark.parametrize("login_r", test_user_data, indirect=True)
def test_login(login_r):
    a = login_r
    print(f"测试用例中login的返回值; {a}")
    assert a != ""

pytest结合YAML

数据量非常大的时候,我们可以将数据存放到外部文件中,使用的时候将文件中的数据读取出来,方便测试数据的管理。数据与测试用例分别管理,可以利用外部数据源 YAML、JSON、Excel、CSV 管理测试数据。

  1. 关于YAML
    可参考文章YAML简介
  2. 在python中引用yaml外部文件
    • 安装PyYAML
pip install PyYAML
  • 使用
    在python中使用yaml.safe_dump()yaml.safe_load()函数将Python值和YAML格式数据相互转换。工作中常常使用 YAML 格式的文件存储测试数据。如1.yaml 文件内容如下:
- 1
- 2
- 3
import yaml
print(yaml.safe_load(open("1.yaml")))
---------------------------------
[1, 2, 3]
  • 数据驱动
    data.yaml文件如下:
-
  - 1
  - 2
- 
  - 20
  - 30

[[1, 2], [20, 30]]

import pytest
import yaml

@pytest.mark.parametrize("a,b", yaml.safe_load(open("data.yml", encoding='utf-8')))
def test_foo(a,b):
      print(f"a + b = {a + b}")

使用外部yaml文件更方便维护测试用例数据

多线程并行与分布式执行


pytest-xdist 是 pytest 分布式执行插件,可以多个 CPU 或主机执行,这款插件允许用户将测试并发执行(进程级并发),插件是动态决定测试用例执行顺序的,为了保证各个测试能在各个独立线程里正确的执行,应该保证测试用例的独立性(这也符合测试用例设计的最佳实践)

  1. 安装
pip install pytest-xdist
  1. 执行
pytest -n auto   
pytest -n [num]

多个 CPU 并行执行用例,需要在 pytest 后面添加 -n 参数,如果参数为 auto,会自动检测系统的 CPU 数目。如果参数为数字,则指定运行测试的处理器进程数。

参考