Python pytest 精通 fixture

dongfanger · 2020年09月23日 · 346 次阅读

首先放一句 “狠话”。

如果你不会 fixture,那么你最好别说自己会 pytest。

(只是为了烘托主题哈,手上的砖头可以放下了,手动滑稽)

fixture 是什么

看看源码

def fixture(
    callable_or_scope=None,
    *args,
    scope="function",
    params=None,
    autouse=False,
    ids=None,
    name=None
):
    """Decorator to mark a fixture factory function.

    This decorator can be used, with or without parameters, to define a
    fixture function.

    The name of the fixture function can later be referenced to cause its
    invocation ahead of running tests: test
    modules or classes can use the ``pytest.mark.usefixtures(fixturename)``
    marker.

    Test functions can directly use fixture names as input
    arguments in which case the fixture instance returned from the fixture
    function will be injected.

    Fixtures can provide their values to test functions using ``return`` or ``yield``
    statements. When using ``yield`` the code block after the ``yield`` statement is executed
    as teardown code regardless of the test outcome, and must yield exactly once.

    :arg scope: the scope for which this fixture is shared, one of
                ``"function"`` (default), ``"class"``, ``"module"``,
                ``"package"`` or ``"session"`` (``"package"`` is considered **experimental**
                at this time).

                This parameter may also be a callable which receives ``(fixture_name, config)``
                as parameters, and must return a ``str`` with one of the values mentioned above.

                See :ref:`dynamic scope` in the docs for more information.

    :arg params: an optional list of parameters which will cause multiple
                invocations of the fixture function and all of the tests
                using it.
                The current parameter is available in ``request.param``.

    :arg autouse: if True, the fixture func is activated for all tests that
                can see it.  If False (the default) then an explicit
                reference is needed to activate the fixture.

    :arg ids: list of string ids each corresponding to the params
                so that they are part of the test id. If no ids are provided
                they will be generated automatically from the params.

    :arg name: the name of the fixture. This defaults to the name of the
                decorated function. If a fixture is used in the same module in
                which it is defined, the function name of the fixture will be
                shadowed by the function arg that requests the fixture; one way
                to resolve this is to name the decorated function
                ``fixture_<fixturename>`` and then use
                ``@pytest.fixture(name='<fixturename>')``.
    """
    if params is not None:
        params = list(params)

    fixture_function, arguments = _parse_fixture_args(
        callable_or_scope,
        *args,
        scope=scope,
        params=params,
        autouse=autouse,
        ids=ids,
        name=name,
    )
    scope = arguments.get("scope")
    params = arguments.get("params")
    autouse = arguments.get("autouse")
    ids = arguments.get("ids")
    name = arguments.get("name")

    if fixture_function and params is None and autouse is False:
        # direct decoration
        return FixtureFunctionMarker(scope, params, autouse, name=name)(
            fixture_function
        )

    return FixtureFunctionMarker(scope, params, autouse, ids=ids, name=name)

总结一下

【定义】

  • fixture 是一个函数,在函数上添加注解@pytest.fixture来定义
  • 定义在 conftest.py 中,无需 import 就可以调用
  • 定义在其他文件中,import 后也可以调用
  • 定义在相同文件中,直接调用

【使用】

  • 第一种使用方式是@pytest.mark.usefixtures(fixturename)(如果修饰 TestClass 能对类中所有方法生效)
  • 第二种使用方式是作为函数参数
  • 第三种使用方式是 autouse(不需要显示调用,自动运行)

conftest.py

我们常常会把 fixture 定义到 conftest.py 文件中。

这是 pytest 固定的文件名,不能自定义。

必须放在 package 下,也就是目录中有__init__.py。

conftest.py 中的 fixture 可以用在当前目录及其子目录,不需要 import,pytest 会自动找。

可以创建多个 conftest.py 文件,同名 fixture 查找时会优先用最近的。

依赖注入

fixture 实现了依赖注入。依赖注入是控制反转(IoC, Inversion of Control)的一种技术形式。

简单理解一下什么是依赖注入和控制反转

实在是妙啊!我们可以在不修改当前函数代码逻辑的情况下,通过 fixture 来额外添加一些处理。

入门示例

# content of ./test_smtpsimple.py
import smtplib

import pytest


@pytest.fixture
def smtp_connection():
    return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)


def test_ehlo(smtp_connection):
    response, msg = smtp_connection.ehlo()
    assert response == 250
    assert 0  # for demo purposes

执行后程序处理逻辑

  1. pytest 找到 test_开头的函数,发现需要名字为 smtp_connection 的 fixture,就去找
  2. 找到之后,调用 smtp_connection(),return 了 SMTP 的实例
  3. 调用 test_ehlo() ,入参smtp_connection等于 fixture return 的值

如果想看文件定义了哪些 fixture,可以使用命令,_前缀的需要跟上-v

pytest --fixtures test_simplefactory.py

fixture scope & order

既然到处都可以定义 fixture,那多了岂不就乱了?

pytest 规定了 fxture 的运行范围和运行顺序。

fixture 的范围通过参数 scope 来指定

@pytest.fixture(scope="module")

默认是 function,可以选择 function, class, module, package 或 session。

fixture 都是在 test 第一次调用时创建,根据 scope 的不同有不同的运行和销毁方式

  • function 每个函数运行一次,函数结束时销毁
  • class 每个类运行一次,类结束时销毁
  • module 每个模块运行一次,模块结束时销毁
  • package 每个包运行一次,包结束时销毁
  • session 每个会话运行一次,会话结束时销毁

fixture 的顺序优先按 scope 从大到小,session > package > module > class > function。

如果 scope 相同,就按 test 调用先后顺序,以及 fixture 之间的依赖关系。

autouse 的 fixture 会优先于相同 scope 的其他 fixture。

示例

import pytest

# fixtures documentation order example
order = []


@pytest.fixture(scope="session")
def s1():
    order.append("s1")


@pytest.fixture(scope="module")
def m1():
    order.append("m1")


@pytest.fixture
def f1(f3):
    order.append("f1")


@pytest.fixture
def f3():
    order.append("f3")


@pytest.fixture(autouse=True)
def a1():
    order.append("a1")


@pytest.fixture
def f2():
    order.append("f2")

def test_order(f1, m1, f2, s1):
    assert order == ["s1", "m1", "a1", "f3", "f1", "f2"]

虽然 test_order() 是按 f1, m1, f2, s1 调用的,但是结果却不是按这个顺序

  1. s1 scope 为 session
  2. m1 scope 为 module
  3. a1 autouse,默认 function,后于 session、module,先于 function 其他 fixture
  4. f3 被 f1 依赖
  5. f1 test_order() 参数列表第 1 个
  6. f2 test_order() 参数列表第 3 个

fixture 嵌套

fixture 装饰的是函数,那函数也有入参咯。

fixture 装饰的函数入参,只能是其他 fixture。

示例,f1 依赖 f3,如果不定义 f3 的话,执行会报错 fixture 'f3' not found

@pytest.fixture
def f1(f3):
    order.append("f1")


@pytest.fixture
def f3():
    order.append("f3")

def test_order(f1):
    pass

从 test 传值给 fixture

借助 request,可以把 test 中的值传递给 fixture。

示例 1,smtp_connection 可以使用 module 中的 smtpserver

# content of conftest.py
import smtplib

import pytest


@pytest.fixture(scope="module")
def smtp_connection(request):
    server = getattr(request.module, "smtpserver", "smtp.gmail.com")
    smtp_connection = smtplib.SMTP(server, 587, timeout=5)
    yield smtp_connection
    print("finalizing {} ({})".format(smtp_connection, server))
    smtp_connection.close()

# content of test_anothersmtp.py
smtpserver = "mail.python.org"  # will be read by smtp fixture


def test_showhelo(smtp_connection):
    assert 0, smtp_connection.helo()

示例 2,结合 request+mark,把 fixt_data 从 test_fixt 传值给了 fixt

import pytest


@pytest.fixture
def fixt(request):
    marker = request.node.get_closest_marker("fixt_data")
    if marker is None:
        # Handle missing marker in some way...
        data = None
    else:
        data = marker.args[0]
    # Do something with the data
    return data


@user16a(42)
def test_fixt(fixt):
    assert fixt == 42

fixture setup / teardown

其他测试框架 unittest/testng,都定义了 setup 和 teardown 函数/方法,用来测试前初始化和测试后清理。

pytest 也有,不过是兼容 unittest 等弄的,不推荐!

from loguru import logger


def setup():
    logger.info("setup")


def teardown():
    logger.info("teardown")


def test():
    pass

建议使用 fixture。

setup,fixture 可以定义 autouse 来实现初始化。

@pytest.fixture(autouse=True)

autouse 的 fixture 不需要调用,会自己运行,和 test 放到相同 scope,就能实现 setup 的效果。

autouse 使用说明

  • autouse 遵循 scope 的规则,scope="session"整个会话只会运行 1 次,其他同理
  • autouse 定义在 module 中,module 中的所有 function 都会用它(如果 scope="module",只运行 1 次,如果 scope="function",会运行多次)
  • autouse 定义在 conftest.py,conftest 覆盖的 test 都会用它
  • autouse 定义在 plugin 中,安装 plugin 的 test 都会用它
  • 在使用 autouse 时需要同时注意 scope 和定义位置

示例,transact 默认 scope 是 function,会在每个 test 函数执行前自动运行

# content of test_db_transact.py
import pytest


class DB:
    def __init__(self):
        self.intransaction = []

    def begin(self, name):
        self.intransaction.append(name)

    def rollback(self):
        self.intransaction.pop()


@pytest.fixture(scope="module")
def db():
    return DB()


class TestClass:
    @pytest.fixture(autouse=True)
    def transact(self, request, db):
        db.begin(request.function.__name__)
        yield
        db.rollback()

    def test_method1(self, db):
        assert db.intransaction == ["test_method1"]

    def test_method2(self, db):
        assert db.intransaction == ["test_method2"]

这个例子不用 autouse,用 conftest.py 也能实现

# content of conftest.py
@pytest.fixture
def transact(request, db):
    db.begin()
    yield
    db.rollback()
@user21res("transact")
class TestClass:
    def test_method1(self):
        ...

teardown,可以在 fixture 中使用 yield 关键字来实现清理。

示例,scope 为 module,在 module 结束时,会执行 yield 后面的 print() 和 smtp_connection.close()

# content of conftest.py
import smtplib

import pytest


@pytest.fixture(scope="module")
def smtp_connection():
    smtp_connection = smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
    yield smtp_connection  # provide the fixture value
    print("teardown smtp")
    smtp_connection.close()

可以使用 with 关键字进一步简化,with 会自动清理上下文,执行 smtp_connection.close()

# content of test_yield2.py
import smtplib

import pytest


@pytest.fixture(scope="module")
def smtp_connection():
    with smtplib.SMTP("smtp.gmail.com", 587, timeout=5) as smtp_connection:
        yield smtp_connection  # provide the fixture value

fixture 参数化

后续会专门讲 “pytest 参数化”,这里就先跳过,请各位见谅啦。

因为我觉得想用 pytest 做参数化,一定是先到参数化的文章里面找,而不是到 fixture。

把这部分放到参数化,更便于以后检索。

简要回顾

本文开头通过源码介绍了 fixture 是什么,并简单总结定义和用法。然后对依赖注入进行了解释,以更好理解 fixture 技术的原理。入门示例给出了官网的例子,以此展开讲了范围、顺序、嵌套、传值,以及初始化和清理的知识。

如果遇到问题,欢迎沟通讨论。

更多实践内容,请关注后续篇章《tep 最佳实践》。

参考资料

https://en.wikipedia.org/wiki/Dependency_injection

https://en.wikipedia.org/wiki/Inversion_of_control

https://docs.pytest.org/en/stable/contents.html#toc

如果你觉得这篇文章写的还不错的话,关注公众号 “测试老树”,你的支持就是我写文章的最大动力。

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册