Pytest官方教程-05-Pytest fixtures:清晰 模块化 易扩展

目录:

  1. 安装及入门
  2. 使用和调用方法
  3. 原有TestSuite使用方法
  4. 断言的编写和报告
  5. Pytest fixtures:清晰 模块化 易扩展
  6. 使用Marks标记测试用例
  7. Monkeypatching/对模块和环境进行Mock
  8. 使用tmp目录和文件
  9. 捕获stdout及stderr输出
  10. 捕获警告信息
  11. 模块及测试文件中集成doctest测试
  12. skip及xfail: 处理不能成功的测试用例
  13. Fixture方法及测试用例的参数化
  14. 缓存: 使用跨执行状态
  15. unittest.TestCase支持
  16. 运行Nose用例
  17. 经典xUnit风格的setup/teardown
  18. 安装和使用插件
  19. 插件编写
  20. 编写钩子(hook)方法
  21. 运行日志
  22. API参考
    1. 方法(Functions)
    2. 标记(Marks)
    3. 钩子(Hooks)
    4. 装置(Fixtures)
    5. 对象(Objects)
    6. 特殊变量(Special Variables)
    7. 环境变量(Environment Variables)
    8. 配置选项(Configuration Options)
  23. 优质集成实践
  24. 片状测试
  25. Pytest导入机制及sys.path/PYTHONPATH
  26. 配置选项
  27. 示例及自定义技巧
  28. Bash自动补全设置

Pytest fixtures:清晰 模块化 易扩展

2.0/2.3/2.4版本新功能
text fixtures的目的是为测试的重复执行提供一个可靠的固定基线。 pytest fixture比经典的xUnit setUp/tearDown方法有着显着的改进:

  • fixtures具有明确的名称,在测试方法/类/模块或整个项目中通过声明使用的fixtures名称来使用。
  • fixtures以模块化方式实现,因为每个fixture名称都会触发调用fixture函数,该fixture函数本身可以使用其它的fixtures。
  • 从简单的单元测试到复杂的功能测试,fixtures的管理允许根据配置和组件选项对fixtures和测试用例进行参数化,或者在测试方法/类/模块或整个测试会话范围内重复使用该fixture。

此外,pytest继续支持经典的xUnit风格的setup方法。 你可以根据需要混合使用两种样式,逐步从经典样式移动到新样式。 您也可以从现有的unittest.TestCase样式或基于nose的项目开始。

Fixtures作为函数参数使用

测试方法可以通过在其参数中使用fixtures名称来接收fixture对象。 每个fixture参数名称所对应的函数,可以通过使用@pytest.fixture注册成为一个fixture函数,来为测试方法提供一个fixture对象。 让我们看一个只包含一个fixture和一个使用它的测试方法的简单独立测试模块:

# ./test_smtpsimple.py内容
import pytest

@pytest.fixture
def smtp_connection():
    import smtplib
    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

这里,test_ehlo需要smtp_connection来提供fixture对象。pytest将发现并调用带@pytest.fixture装饰器的smtp_connection fixture函数。 运行测试如下所示:

$ pytest test_smtpsimple.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
rootdir: $REGENDOC_TMPDIR, inifile:
collected 1 item

test_smtpsimple.py F                                                 [100%]

================================= FAILURES =================================
________________________________ test_ehlo _________________________________

smtp_connection = 

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

test_smtpsimple.py:11: AssertionError
========================= 1 failed in 0.12 seconds =========================

在测试失败的回溯信息中,我们看到测试方法是使用smtp_connection参数调用的,即由fixture函数创建的smtplib.SMTP()实例。测试用例在我们故意的assert 0上失败。以下是pytest用这种方式调用测试方法使用的确切协议:

Fixtures: 依赖注入的主要例子

Fixtures允许测试方法能轻松引入预先定义好的初始化准备函数,而无需关心导入/设置/清理方法的细节。 这是依赖注入的一个主要示例,其中fixture函数的功能扮演”注入器“的角色,测试方法来“消费”这些fixture对象。

conftest.py: 共享fixture函数

如果在测试中需要使用多个测试文件中的fixture函数,则可以将其移动到conftest.py文件中,所需的fixture对象会自动被pytest发现,而不需要再每次导入。 fixture函数的发现顺序从测试类开始,然后是测试模块,然后是conftest.py文件,最后是内置和第三方插件。

你还可以使用conftest.py文件来实现本地每个目录的插件。

共享测试数据

如果要使用数据文件中的测试数据,最好的方法是将这些数据加载到fixture函数中以供测试方法注入使用。这利用到了pytest的自动缓存机制。

另一个好方法是在tests文件夹中添加数据文件。 还有社区插件可用于帮助处理这方面的测试,例如:pytest-datadirpytest-datafiles

生效范围:在测试类/测试模块/测试会话中共享fixture对象

由于fixtures对象需要连接形成依赖网,而通常创建时间比较长。 扩展前面的示例,我们可以在@pytest.fixture调用中添加scope ="module"参数,以使每个测试模块只调用一次修饰的smtp_connection fixture函数(默认情况下,每个测试函数调用一次)。 因此,测试模块中的多个测试方法将各自注入相同的smtp_connectionfixture对象,从而节省时间。scope参数的可选值包括:function(函数), class(类), module(模块), package(包)及 session(会话)。

下一个示例将fixture函数放入单独的conftest.py文件中,以便来自目录中多个测试模块的测试可以访问fixture函数:

# conftest.py文件内容
import pytest
import smtplib

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

fixture对象的名称依然是smtp_connection,你可以通过在任何测试方法或fixture函数(在conftest.py所在的目录中或下面)使用参数smtp_connection作为输入参数来访问其结果:

# test_module.py文件内容

def test_ehlo(smtp_connection):
    response, msg = smtp_connection.ehlo()
    assert response == 250
    assert b"smtp.gmail.com" in msg
    assert 0  # for demo purposes

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

我们故意插入失败的assert 0语句,以便检查发生了什么,运行测试并查看结果:

$ pytest test_module.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR, inifile:
collected 2 items

test_module.py FF                                                    [100%]

================================= FAILURES =================================
________________________________ test_ehlo _________________________________

smtp_connection = 

    def test_ehlo(smtp_connection):
        response, msg = smtp_connection.ehlo()
        assert response == 250
        assert b"smtp.gmail.com" in msg
>       assert 0  # for demo purposes
E       assert 0

test_module.py:6: AssertionError
________________________________ test_noop _________________________________

smtp_connection = 

    def test_noop(smtp_connection):
        response, msg = smtp_connection.noop()
        assert response == 250
>       assert 0  # for demo purposes
E       assert 0

test_module.py:11: AssertionError
========================= 2 failed in 0.12 seconds =========================

你会看到两个assert 0失败信息,更重要的是你还可以看到相同的(模块范围的)smtp_connection对象被传递到两个测试方法中,因为pytest在回溯信息中显示传入的参数值。 因此,使用smtp_connection的两个测试方法运行速度与单个函数一样快,因为它们重用了相同的fixture对象。

如果您决定要使用session(会话,一次运行算一次会话)范围的smtp_connection对象,则只需如下声明:

@pytest.fixture(scope="session")
def smtp_connection():
    # the returned fixture value will be shared for
    # all tests needing it
    ...

最后,class(类)范围将为每个测试类调用一次fixture对象。

注意:
Pytest一次只会缓存一个fixture实例。 这意味着当使用参数化fixture时,pytest可能会在给定范围内多次调用fixture函数。

package(包)范围的fixture(实验性功能)
3.7版本新功能
在pytest 3.7中,引入了包范围。 当包的最后一次测试结束时,最终确定包范围的fixture函数。

警告:
此功能是实验性的,如果在获得更多使用后发现隐藏的角落情况或此功能的严重问题,可能会在将来的版本中删除。

谨慎使用此新功能,请务必报告您发现的任何问题。

高范围的fixture函数优先实例化

3.5版本新功能
在测试函数的fixture对象请求中,较高范围的fixture(例如session会话级)较低范围的fixture(例如function函数级或class类级优先执行。相同范围的fixture对象的按引入的顺序及fixtures之间的依赖关系按顺序调用。

请考虑以下代码:

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

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

@pytest.fixture
def f1(tmpdir):
    pass

@pytest.fixture
def f2():
    pass

def test_foo(f1, m1, f2, s1):
    ...

test_foo中fixtures将按以下顺序执行:

  1. s1:是最高范围的fixture(会话级)
  2. m1:是第二高的fixture(模块级)
  3. tmpdir:是一个函数级的fixture,f1依赖它,因此它需要在f1前调用
  4. f1:是test_foo参数列表中第一个函数范围的fixture。
  5. f2:是test_foo参数列表中最后一个函数范围的fixture。

fixture结束/执行teardown代码

当fixture超出范围时,通过使用yield语句而不是return,pytest支持fixture执行特定的teardown代码。yield语句之后的所有代码都视为teardown代码:

# 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()

无论测试的异常状态如何,printsmtp.close()语句将在模块中的最后一个测试完成执行时执行。

让我们执行一下(上文的test_module.py):

$ pytest -s -q --tb=no
FFteardown smtp

2 failed in 0.12 seconds

我们看到smtp_connection实例在两个测试完成执行后完成。 请注意,如果我们使用scope ='function'修饰我们的fixture函数,那么每次单个测试都会进行fixture的setup和teardown。 在任何一种情况下,测试模块本身都不需要改变或了解fixture函数的这些细节。

请注意,我们还可以使用with语句无缝地使用yield语法:

# 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

测试结束后, smtp_connection连接将关闭,因为当with语句结束时,smtp_connection对象会自动关闭。

请注意,如果在设置代码期间(yield关键字之前)发生异常,则不会调用teardown代码(在yield之后)。
执行teardown代码的另一种选择是利用请求上下文对象的addfinalizer方法来注册teardown函数。
以下是smtp_connectionfixture函数更改为使用addfinalizer进行teardown:

# content of conftest.py
import smtplib
import pytest


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

    def fin():
        print("teardown smtp_connection")
        smtp_connection.close()

    request.addfinalizer(fin)
    return smtp_connection  # provide the fixture value

yieldaddfinalizer方法在测试结束后调用它们的代码时的工作方式类似,但addfinalizer相比yield有两个主要区别:

  1. 使用addfinalizer可以注册多个teardown功能。
  2. 无论fixture中setup代码是否引发异常,都将始终调用teardown代码。 即使其中一个资源无法创建/获取,也可以正确关闭fixture函数创建的所有资源:
@pytest.fixture
def equipments(request):
    r = []
    for port in ('C1', 'C3', 'C28'):
        equip = connect(port)
        request.addfinalizer(equip.disconnect)
        r.append(equip)
    return r

在上面的示例中,如果“C28”因异常而失败,则“C1”和“C3”仍将正确关闭。 当然,如果在注册finalize函数之前发生异常,那么它将不会被执行。

Fixtures中使用测试上下文的内省信息

Fixtures工厂方法

Fixtures参数化

使用参数化fixtures标记

模块化:在fixture函数中使用fixtures功能

使用fixture实例自动组织测试用例

在类/模块/项目中使用fixtures

自动使用fixtures(xUnit 框架的setup固定方法)

不同级别的fixtures的覆盖(优先级)

相对于在较大范围的测试套件中的Test Fixtures方法,在较小范围子套件你可能需要重写和覆盖外层的Test Fixtures方法,从而保持测试代码的可读性和可维护性。

在文件夹级别(通过conftest文件)重写fixtures方法

假设用例目录结构为:

tests/
    __init__.py

    conftest.py
        # content of tests/conftest.py
        import pytest

        @pytest.fixture
        def username():
            return 'username'

    test_something.py
        # content of tests/test_something.py
        def test_username(username):
            assert username == 'username'

    subfolder/
        __init__.py

        conftest.py
            # content of tests/subfolder/conftest.py
            import pytest

            @pytest.fixture
            def username(username):
                return 'overridden-' + username

        test_something.py
            # content of tests/subfolder/test_something.py
            def test_username(username):
                assert username == 'overridden-username'

你可以看到, 基础/上级fixtures方法可以通过子文件夹下的con
ftest.py中同名的fixtures方法覆盖, 非常简单, 只需要按照上面的例子使用即可.

在测试模块级别重写fixtures方法

假设用例文件结构如下:

tests/
    __init__.py

    conftest.py
        # content of tests/conftest.py
        @pytest.fixture
        def username():
            return 'username'

    test_something.py
        # content of tests/test_something.py
        import pytest

        @pytest.fixture
        def username(username):
            return 'overridden-' + username

        def test_username(username):
            assert username == 'overridden-username'

    test_something_else.py
        # content of tests/test_something_else.py
        import pytest

        @pytest.fixture
        def username(username):
            return 'overridden-else-' + username

        def test_username(username):
            assert username == 'overridden-else-username'

上面的例子中, 用例模块(文件)中的fixture方法会覆盖文件夹conftest.py中同名的fixtures方法

在直接参数化方法中覆盖fixtures方法

假设用例文件结构为:

tests/
    __init__.py

    conftest.py
        # content of tests/conftest.py
        import pytest

        @pytest.fixture
        def username():
            return 'username'

        @pytest.fixture
        def other_username(username):
            return 'other-' + username

    test_something.py
        # content of tests/test_something.py
        import pytest

        @pytest.mark.parametrize('username', ['directly-overridden-username'])
        def test_username(username):
            assert username == 'directly-overridden-username'

        @pytest.mark.parametrize('username', ['directly-overridden-username-other'])
        def test_username_other(other_username):
            assert other_username == 'other-directly-overridden-username-other'

在上面的示例中,username fixture方法的结果值被参数化值覆盖。 请注意,即使测试不直接使用(也未在函数原型中提及),也可以通过这种方式覆盖fixture的值。

使用非参数化fixture方法覆盖参数化fixtures方法, 反之亦然

假设用例结构为:

tests/
    __init__.py

    conftest.py
        # content of tests/conftest.py
        import pytest

        @pytest.fixture(params=['one', 'two', 'three'])
        def parametrized_username(request):
            return request.param

        @pytest.fixture
        def non_parametrized_username(request):
            return 'username'

    test_something.py
        # content of tests/test_something.py
        import pytest

        @pytest.fixture
        def parametrized_username():
            return 'overridden-username'

        @pytest.fixture(params=['one', 'two', 'three'])
        def non_parametrized_username(request):
            return request.param

        def test_username(parametrized_username):
            assert parametrized_username == 'overridden-username'

        def test_parametrized_username(non_parametrized_username):
            assert non_parametrized_username in ['one', 'two', 'three']

    test_something_else.py
        # content of tests/test_something_else.py
        def test_username(parametrized_username):
            assert parametrized_username in ['one', 'two', 'three']

        def test_username(non_parametrized_username):
            assert non_parametrized_username == 'username'

在上面的示例中,使用非参数化fixture方法覆盖参数化fixture方法,以及使用参数化fixture覆盖非参数化fixture以用于特定测试模块。 这同样适用于文件夹级别的fixtures方法。

你可能感兴趣的:(Pytest官方教程-05-Pytest fixtures:清晰 模块化 易扩展)